slik 0.2.0

A binder-first Motion-inspired animation framework for Leptos
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
# Slik

**Slik** is a binder-first, Motion-inspired animation framework for **Leptos**.

It is built around a small numeric motion core with three public layers:

- `use_motion` for binding animation directly to typed `NodeRef`s
- `slik::html::*` for thin motion-enabled HTML component sugar
- `MotionValue` for low-level animated scalar state

v0.2 is a clean rewrite over the earlier wrapper-first MVP. This README documents
the **current** API only.

---

## Why Slik exists

Leptos already gives you precise reactive control over state and DOM ownership.
Slik focuses on the missing piece: making motion feel native to that model.

The design goals are intentionally narrow:

- Leptos-first reactivity
- binder-first architecture instead of wrapper-first architecture
- explicit numeric motion props
- spring, tween, and keyframe transitions
- per-property transition overrides
- real browser runtime on `wasm32`
- clean native checking and testing outside the browser

If you are familiar with `motion.dev`, the mental model is similar: declare a
target, and Slik animates from the current value to the new one. The difference
is that v0.2 is grounded in typed Leptos `NodeRef` binding rather than a
wrapper-centric component runtime.

---

## What Slik is

Slik is a **small, focused motion system for Leptos apps**.

It is good at:

- entry animations
- hover and press interactions
- fades, lifts, slides, scale, and rotation
- per-property motion tuning
- interruptible numeric keyframes
- animated counters, meters, and dashboards
- binding motion to real HTML and SVG nodes

## What Slik is not

v0.2 is intentionally not trying to cover the full Motion surface area.

It does **not** currently include:

- variants
- exit / presence orchestration
- gesture APIs
- layout animation
- color interpolation
- arbitrary CSS string interpolation
- SVG sugar components
- a generic catch-all motion element API

That scope is intentional. v0.2 is the architectural foundation.

---

## At a Glance

| Layer | Use it when | Main API |
|---|---|---|
| Binder | You want maximum control on a real node | `use_motion`, `MotionOptions` |
| Sugar | You want ergonomic motion-enabled HTML components | `slik::html::*` |
| Scalar | You want a single animated `f64` | `MotionValue` |

---

## Installation

If you are using Slik from this workspace or before a public crates.io release:

```toml
[dependencies]
slik = { path = "crates/slik", features = ["csr"] }
leptos = { version = "0.8", features = ["csr"] }
```

Slik mirrors Leptos target features:

- `csr`
- `hydrate`
- `ssr`

Pick the same feature set you use for `leptos`.

---

## Quick Start

The ergonomic starting point is usually the HTML sugar layer.

```rust
use leptos::prelude::*;
use slik::html::MotionDiv;
use slik::prelude::*;

#[component]
fn FadeInCard() -> impl IntoView {
    view! {
        <MotionDiv
            initial=Signal::derive(|| MotionStyle::new().opacity(0.0).y(24.0))
            animate=MotionStyle::new().opacity(1.0).y(0.0)
            transition=TransitionMap::new(
                Transition::tween(0.35, Easing::EaseOut).expect("valid tween"),
            )
            attr:style="padding:1rem; border-radius:14px; background:#eef2ff; border:1px solid #c7d2fe"
        >
            "Hello from Slik v0.2"
        </MotionDiv>
    }
}
```

This mounts with:

- `opacity: 0 -> 1`
- `translateY: 24px -> 0px`

using a tweened opacity + transform target on a real `<div>`, with no extra
wrapper inserted by Slik.

---

## The v0.2 Mental Model

Slik v0.2 is easiest to understand through four concepts.

### 1. `MotionStyle`

`MotionStyle` is the sparse target definition for supported motion properties.

```rust
MotionStyle::new()
    .opacity(1.0)
    .x(24.0)
    .scale(1.04)
```

Each property is optional. If you do not set a property, it is absent from that
style snapshot.

### 2. `Transition`

`Transition` defines how a value moves to its target.

Slik ships with three transition families:

- `Transition::spring()`
- `Transition::tween(duration_secs, easing)`
- `Transition::keyframes(keyframes, duration_secs)`

### 3. `use_motion`

`use_motion` is the canonical v0.2 API.

It binds motion to a typed Leptos `NodeRef`, owns the target node's inline
`opacity`, `transform`, and `will-change`, and keeps them synced to reactive
motion values.

### 4. `MotionValue`

`MotionValue` is the low-level scalar primitive.

Use it when you want animation without binding a full `MotionStyle` to a DOM
node.

---

## Binder-First Example

The binder is the foundation. Sugar components are layered on top of it.

```rust
use leptos::prelude::*;
use slik::prelude::*;

#[component]
fn BinderCard() -> impl IntoView {
    let node_ref = NodeRef::<leptos::html::Div>::new();
    let raised = RwSignal::new(false);

    let _motion = use_motion(
        node_ref,
        MotionOptions {
            initial: Some(Signal::derive(|| MotionStyle::new().opacity(0.0).y(20.0))),
            animate: Signal::derive(move || {
                if raised.get() {
                    MotionStyle::new().opacity(1.0).y(-6.0).scale(1.02)
                } else {
                    MotionStyle::new().opacity(1.0).y(0.0).scale(1.0)
                }
            }),
            transition: TransitionMap::new(Transition::spring_bouncy())
                .with(
                    MotionProp::Opacity,
                    Transition::tween(0.22, Easing::EaseOut).expect("valid tween"),
                )
                .into(),
            reduced_motion: MaybeProp::default(),
        },
    );

    view! {
        <div style="display:flex; gap:0.75rem; align-items:center">
            <button on:click=move |_| raised.update(|value| *value = !*value)>
                "Toggle"
            </button>
            <div
                node_ref=node_ref
                style="padding:1rem; border-radius:14px; background:#fef3c7; border:1px solid #f59e0b"
            >
                "Motion bound directly to this div"
            </div>
        </div>
    }
}
```

This is the core of the library:

- you create a real `NodeRef`
- you describe `initial` and `animate`
- Slik manages the motion runtime
- your element remains your element

No polymorphic wrapper, no hidden extra node.

### `MotionHandle`

`use_motion` returns a `MotionHandle`.

That handle exposes dense per-property `MotionValue`s:

```rust
let handle = use_motion(node_ref, options);
let x_value = handle.values.get(MotionProp::X);
```

This is useful when you want binder-driven DOM motion plus direct access to one
or more animated scalar channels.

---

## Thin HTML Sugar

The HTML sugar layer exists for ergonomics, not as a separate architecture.

Every motion HTML component is a thin wrapper over `use_motion`.

```rust
use leptos::prelude::*;
use slik::html::MotionButton;
use slik::prelude::*;

#[component]
fn HoverButton() -> impl IntoView {
    let hovered = RwSignal::new(false);

    let animate = Signal::derive(move || {
        if hovered.get() {
            MotionStyle::new().scale(1.04).y(-2.0)
        } else {
            MotionStyle::new().scale(1.0).y(0.0)
        }
    });

    view! {
        <MotionButton
            animate=animate
            transition=TransitionMap::new(Transition::spring_bouncy())
            on:mouseenter=move |_| hovered.set(true)
            on:mouseleave=move |_| hovered.set(false)
            attr:style="padding:0.8rem 1rem; border:none; border-radius:12px; background:#111827; color:white; cursor:pointer"
        >
            "Hover me"
        </MotionButton>
    }
}
```

### Important attribute forwarding note

Motion HTML components use Leptos `AttributeInterceptor`.

That means:

- event handlers such as `on:click` work normally
- plain DOM attributes should be forwarded with `attr:*`

Examples:

```rust
<MotionDiv attr:style="padding:1rem" attr:id="hero-card">
    "..."
</MotionDiv>
```

---

## Available Motion HTML Components

Current sugar coverage includes:

- Structure: `MotionArticle`, `MotionAside`, `MotionDiv`, `MotionFooter`, `MotionHeader`, `MotionMain`, `MotionNav`, `MotionSection`
- Headings: `MotionH1`, `MotionH2`, `MotionH3`, `MotionH4`, `MotionH5`, `MotionH6`
- Text: `MotionBlockquote`, `MotionCode`, `MotionEm`, `MotionP`, `MotionPre`, `MotionSmall`, `MotionSpan`, `MotionStrong`
- Lists: `MotionDl`, `MotionLi`, `MotionOl`, `MotionUl`
- Forms and controls: `MotionButton`, `MotionDetails`, `MotionFieldset`, `MotionForm`, `MotionLabel`, `MotionLegend`, `MotionSummary`, `MotionTextarea`
- Other content: `MotionA`, `MotionFigcaption`, `MotionFigure`

If you need an element that does not yet have sugar, the binder layer is already
more general than the current macro list.

---

## Supported Motion Properties

Slik currently supports these numeric motion props:

| Property | Meaning | Units |
|---|---|---|
| `opacity` | CSS opacity | unitless |
| `x` | horizontal translation | px |
| `y` | vertical translation | px |
| `scale` | uniform scale | unitless |
| `scale_x` | x-axis scale multiplier | unitless |
| `scale_y` | y-axis scale multiplier | unitless |
| `rotate` | rotation | deg |

### Transform composition order

Transforms are composed in this fixed order:

```text
translateX -> translateY -> scale -> rotate
```

Axis-specific scale multiplies the uniform scale:

```text
effective_scale_x = scale * scale_x
effective_scale_y = scale * scale_y
```

So this:

```rust
MotionStyle::new().scale(1.2).scale_x(0.8)
```

produces:

```text
scale(0.96, 1.2)
```

---

## `initial` vs `animate`

### `initial`

`initial` seeds the starting visual state for owned properties.

### `animate`

`animate` is the live target snapshot.

Example:

```rust
<MotionDiv
    initial=Signal::derive(|| MotionStyle::new().opacity(0.0).y(20.0))
    animate=MotionStyle::new().opacity(1.0).y(0.0)
>
    "Fade + slide on mount"
</MotionDiv>
```

If `initial` is omitted, the first `animate` snapshot becomes the seed state.

That means:

- no mount transition unless `initial` differs from `animate`
- the binder starts from the first known target by default

### Property ownership semantics

Once a property appears in either `initial` or `animate`, that property becomes
**owned** by the binding.

In practice this means:

- Slik keeps writing that property's current animated value
- omitting a previously owned property from a later `animate` snapshot does not
  implicitly hand control back to other inline styles

This is deliberate. It avoids ambiguous DOM ownership after motion has claimed a
property.

---

## Reactive Targets

Reactive motion targets are a natural fit for Leptos signals and memos.

```rust
use leptos::prelude::*;
use slik::html::MotionDiv;
use slik::prelude::*;

#[component]
fn ReactiveCard() -> impl IntoView {
    let expanded = RwSignal::new(false);

    let target = Signal::derive(move || {
        if expanded.get() {
            MotionStyle::new().scale(1.04).y(-4.0)
        } else {
            MotionStyle::new().scale(1.0).y(0.0)
        }
    });

    view! {
        <div style="display:flex; gap:0.75rem; align-items:center">
            <button on:click=move |_| expanded.update(|value| *value = !*value)>
                "Toggle"
            </button>
            <MotionDiv
                animate=target
                attr:style="padding:1rem; border-radius:14px; background:#ecfccb; border:1px solid #84cc16"
            >
                "Reactive motion target"
            </MotionDiv>
        </div>
    }
}
```

When the signal changes, Slik retargets from the current sampled value.

---

## Transitions

Slik has three transition families.

### Spring

The default transition is a spring.

```rust
Transition::spring()
```

Included presets:

```rust
Transition::spring()
Transition::spring_bouncy()
Transition::spring_gentle()
Transition::spring_custom(stiffness, damping, mass)?
```

Use springs for:

- hover interactions
- press feedback
- panels and cards
- UI motion that should feel responsive and physical

`spring_custom` is validated and returns `Result<Transition, TransitionError>`.

### Tween

Tweens run for a fixed duration with cubic-bezier easing.

```rust
let tween = Transition::tween(0.35, Easing::EaseInOut)?;
```

Available easings:

```rust
Easing::Linear
Easing::Ease
Easing::EaseIn
Easing::EaseOut
Easing::EaseInOut
Easing::Snappy
Easing::Custom(x1, y1, x2, y2)
```

Use tweens for:

- fades
- deterministic micro-interactions
- motion where a fixed duration matters more than spring feel

`tween` is validated and returns `Result<Transition, TransitionError>`.

### Keyframes

Keyframes are literal numeric sequences over normalized progress from `0.0` to
`1.0`.

Each keyframe has:

- an `offset`
- a `value`
- an easing used for the segment that ends at that keyframe

Helpers:

```rust
Keyframe::current(offset)
Keyframe::absolute(offset, value)
Keyframe::target(offset)
```

Example:

```rust
let pulse = Transition::keyframes(
    vec![
        Keyframe::current(0.0),
        Keyframe::absolute(0.35, 1.24).ease(Easing::EaseOut),
        Keyframe::absolute(0.7, 1.06).ease(Easing::EaseInOut),
        Keyframe::target(1.0).ease(Easing::EaseOut),
    ],
    0.55,
)?;
```

This means:

- start from the live current value
- overshoot to `1.24`
- settle toward `1.06`
- finish at the requested target

### Keyframe validation rules

A keyframe transition must satisfy all of these:

- at least 2 keyframes
- first offset must be `0.0`
- last offset must be `1.0`
- offsets must strictly increase
- absolute values must be finite
- final keyframe must be `Keyframe::target(1.0)`

That final target requirement is intentional. It keeps retargeting semantics
explicit and interruptible.

---

## Per-Property Transitions

Use `TransitionMap` when different properties should move differently.

```rust
let transitions = TransitionMap::new(Transition::spring_bouncy())
    .with(
        MotionProp::Opacity,
        Transition::tween(0.25, Easing::EaseOut).expect("valid tween"),
    )
    .with(
        MotionProp::Rotate,
        Transition::tween(0.5, Easing::Snappy).expect("valid tween"),
    );
```

This lets you do things like:

- spring translation
- tween opacity
- snap rotation differently from position

Example:

```rust
use leptos::prelude::*;
use slik::html::MotionDiv;
use slik::prelude::*;

#[component]
fn MixedTransitions() -> impl IntoView {
    let expanded = RwSignal::new(false);

    let animate = Signal::derive(move || {
        if expanded.get() {
            MotionStyle::new().scale(1.25).rotate(180.0).opacity(0.7)
        } else {
            MotionStyle::new().scale(1.0).rotate(0.0).opacity(1.0)
        }
    });

    let transitions = TransitionMap::new(Transition::spring_bouncy())
        .with(
            MotionProp::Opacity,
            Transition::tween(0.35, Easing::EaseOut).expect("valid tween"),
        )
        .with(
            MotionProp::Rotate,
            Transition::tween(0.55, Easing::Snappy).expect("valid tween"),
        );

    view! {
        <div style="display:flex; gap:0.75rem; align-items:center">
            <button on:click=move |_| expanded.update(|value| *value = !*value)>
                "Transform"
            </button>
            <MotionDiv
                animate=animate
                transition=transitions
                attr:style="width:60px; height:60px; background:#0f766e; border-radius:10px"
            />
        </div>
    }
}
```

---

## Reduced Motion

Slik exposes both system-level reduced-motion state and per-binding policy.

### Browser preference signal

```rust
let prefers_reduced_motion = use_reduced_motion();
```

This mirrors the browser's `prefers-reduced-motion` media query.

### Binding policy

`ReducedMotionConfig` controls how a specific motion binding responds:

- `ReducedMotionConfig::Auto`
- `ReducedMotionConfig::Always`
- `ReducedMotionConfig::Never`

Example:

```rust
<MotionDiv
    animate=target
    transition=TransitionMap::new(Transition::spring_bouncy())
    reduced_motion=ReducedMotionConfig::Always
    attr:style="width:48px; height:48px; background:#111827; border-radius:12px"
/>
```

`Always` forces immediate jumps. `Auto` follows the browser preference. `Never`
ignores it.

---

## Low-Level API: `MotionValue`

`MotionValue` animates a single `f64`.

Use it when:

- you want animated numeric state without DOM binding
- you want to drive text, counters, or readouts
- the binder layer is too high-level for the job

Example:

```rust
use leptos::prelude::*;
use slik::prelude::*;

#[component]
fn AnimatedCounter() -> impl IntoView {
    let count = RwSignal::new(0.0_f64);
    let display = MotionValue::new(0.0, Transition::spring());

    Effect::new(move |_| {
        display.set_target(count.get());
    });

    view! {
        <div style="display:flex; gap:0.75rem; align-items:center">
            <button on:click=move |_| count.update(|value| *value -= 1.0)>"-"</button>
            <span>{move || format!("{:.1}", display.get())}</span>
            <button on:click=move |_| count.update(|value| *value += 1.0)>"+"</button>
        </div>
    }
}
```

### `MotionValue` methods

```rust
MotionValue::new(initial, transition)
value.get()
value.get_untracked()
value.target()
value.is_animating()
value.is_animating_untracked()
value.set_target(next)
value.jump(next)
value.stop()
value.signal()
```

### Semantics

- `set_target(v)` animates from the current sampled value toward `v`
- spring retargeting preserves spring momentum
- tween and keyframe retargeting restart from the current sampled value
- `jump(v)` snaps immediately and clears active animation
- `stop()` halts animation at the current sampled value

---

## DOM Behavior

### No wrapper node

This is the most important v0.2 shift.

Slik no longer centers the library around a dedicated wrapper component. The
binder targets real nodes directly, and the HTML sugar components simply create
those nodes for you.

### HTML and SVG binding

`use_motion` supports style-capable HTML and SVG nodes.

That means:

- HTML binding is covered by both `use_motion` and `slik::html::*`
- SVG binding is available through the binder layer today
- HTML sugar is explicit; SVG sugar does not exist yet

### DOM properties owned by Slik

The binder writes:

- `opacity`
- `transform`
- `will-change`

`will-change` is only emitted for actively animating groups:

- `opacity`
- `transform`

It is not left on permanently after animation settles.

---

## Browser and Native Runtime Behavior

Slik is designed to be pleasant in a mixed Rust + wasm workflow.

In practice:

- the real animation loop runs in the browser on `wasm32`
- the browser runtime is driven by `requestAnimationFrame`
- native `cargo check`, `cargo test`, and workspace builds remain straightforward
- on non-wasm targets, animation requests snap immediately to the latest target

This is deliberate. It keeps the browser behavior correct without making native
tooling awkward.

---

## API Overview

### `slik::prelude::*`

The prelude re-exports the main working surface:

| Item | Purpose |
|---|---|
| `use_motion` | binder-first motion hook |
| `use_reduced_motion` | browser reduced-motion signal |
| `MotionOptions` | binder configuration |
| `MotionHandle` | handle returned by `use_motion` |
| `MotionValues` | dense per-property motion values |
| `ReducedMotionConfig` | per-binding reduced-motion policy |
| `MotionStyle` | sparse motion target definition |
| `MotionProp` | explicit property enum |
| `MotionValue` | low-level animated scalar |
| `Transition` | spring, tween, keyframe families |
| `TransitionMap` | default + per-property transitions |
| `TransitionError` | spring/tween validation errors |
| `Easing` | easing presets and custom bezier |
| `Keyframe` | keyframe builder |
| `KeyframeValue` | current / absolute / target keyframe values |
| `KeyframeTransition` | validated keyframe sequence |
| `KeyframeError` | keyframe validation errors |

### `slik::html`

Use this module when you want motion-enabled HTML components such as:

- `MotionDiv`
- `MotionButton`
- `MotionSection`
- `MotionMain`
- `MotionP`
- `MotionH1` through `MotionH6`

---

## Current Limitations

The current scope is intentionally lean.

Notable constraints today:

- numeric properties only
- no layout animation
- no color interpolation
- no arbitrary CSS string interpolation
- no exit / presence orchestration
- no variants
- no SVG sugar components
- sugar coverage is explicit, not open-ended

These are reasonable next-step candidates, but they are not required to use the
current v0.2 foundation effectively.

---

## Design Choices in v0.2

These choices are deliberate.

### Binder-first, sugar-second

`use_motion` is the architectural center.

The sugar layer exists because ergonomics matter, but it is intentionally a thin
layer over the binder instead of a competing runtime.

### Numeric-only motion surface

All current motion values are `f64`.

That keeps interpolation simple, predictable, and easy to test.

### Explicit property enum

Per-property overrides use `MotionProp`, not string keys.

That avoids typo-driven configuration mistakes.

### Keyframe target is explicit

The final keyframe must target `KeyframeValue::Target`.

That keeps interrupt and retarget behavior coherent.

---

## Showcase

The repository includes a working showcase in:

```text
examples/showcase/src/main.rs
```

It demonstrates:

- binder-first entry animation
- HTML sugar components
- tween targets
- keyframe pulse
- `MotionValue` counters
- per-property overrides
- reduced-motion policy
- an interactive `MotionButton`

---

## Status

Slik v0.2 is the first real public-facing foundation for the crate.

It is intentionally narrow, but the pieces that are present are meant to be
coherent:

- typed DOM binding
- thin ergonomic sugar
- explicit motion properties
- predictable numeric interpolation
- validated transition inputs
- browser-aware reduced-motion handling
- a low-level primitive when the binder layer is too high-level

If you understand this README, you understand the current shape of the crate.

---

## License and Contributions

MIT.

Contributions are currently not being actively solicited while the roadmap from
v0.2 toward v1.0 is still being shaped.

Issues, forks, and local experimentation are welcome.