@motion.page/sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
- # @motion/sdk
1
+ # @motion.page/sdk
2
2
 
3
- A high-performance CSS animation SDK with a declarative, config-based API. Zero runtime dependencies.
3
+ A high-performance animation SDK with a declarative API. Scroll-triggered animations, page transitions, custom cursors, gesture controls, text splitting, and more — zero runtime dependencies.
4
4
 
5
- ![Version](https://img.shields.io/badge/version-0.1.0-blue) ![Bundle Size](https://img.shields.io/badge/bundle-TBD-green) ![License](https://img.shields.io/badge/license-Proprietary-red)
5
+ [![npm version](https://img.shields.io/npm/v/@motion.page/sdk)](https://www.npmjs.com/package/@motion.page/sdk)
6
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@motion.page/sdk)](https://bundlephobia.com/package/@motion.page/sdk)
7
+ [![License](https://img.shields.io/badge/license-FSL--1.1--Apache--2.0-blue)](./LICENSE)
6
8
 
7
9
  ---
8
10
 
@@ -11,6 +13,7 @@ A high-performance CSS animation SDK with a declarative, config-based API. Zero
11
13
  - [Installation](#installation)
12
14
  - [Quick Start](#quick-start)
13
15
  - [Core Concept](#core-concept)
16
+ - [Implicit Values](#implicit-values)
14
17
  - [API Reference](#api-reference)
15
18
  - [Motion()](#motion-function)
16
19
  - [Motion Static Methods](#motion-static-methods)
@@ -30,11 +33,20 @@ A high-performance CSS animation SDK with a declarative, config-based API. Zero
30
33
  ## Installation
31
34
 
32
35
  ```bash
33
- # Not published to npm used internally within the Motion.page monorepo.
34
- # The package name is @motion/sdk
36
+ npm install @motion.page/sdk
37
+ # or
38
+ bun add @motion.page/sdk
39
+ # or
40
+ yarn add @motion.page/sdk
41
+ # or
42
+ pnpm add @motion.page/sdk
35
43
  ```
36
44
 
37
- For direct browser use, see [Browser Build](#browser-build).
45
+ 📖 Full documentation and interactive examples: [motion.page](https://motion.page)
46
+
47
+ > **⚠️ Browser-only:** This SDK requires a browser environment (`document`, `window`). In SSR frameworks (Next.js, Nuxt, Astro), wrap SDK calls in `useEffect`, `onMounted`, or client-side scripts.
48
+
49
+ For direct browser use without a bundler, see [Browser Build](#browser-build).
38
50
 
39
51
  ---
40
52
 
@@ -43,12 +55,11 @@ For direct browser use, see [Browser Build](#browser-build).
43
55
  ### Basic Animation
44
56
 
45
57
  ```ts
46
- import { Motion } from '@motion/sdk';
58
+ import { Motion } from '@motion.page/sdk';
47
59
 
48
60
  // Fade in and slide up
49
61
  Motion('hero-intro', '#hero', {
50
62
  from: { opacity: 0, y: 50 },
51
- to: { opacity: 1, y: 0 },
52
63
  duration: 0.8,
53
64
  ease: 'power2.out',
54
65
  }).play();
@@ -60,7 +71,6 @@ Motion('hero-intro', '#hero', {
60
71
  // Scrub animation progress to scroll position
61
72
  Motion('scroll-reveal', '.card', {
62
73
  from: { opacity: 0, y: 40 },
63
- to: { opacity: 1, y: 0 },
64
74
  duration: 0.6,
65
75
  }).onScroll({ scrub: true, start: 'top 80%', end: 'top 30%' });
66
76
  ```
@@ -84,13 +94,11 @@ Motion('intro-sequence', [
84
94
  {
85
95
  target: '.title',
86
96
  from: { opacity: 0, y: -30 },
87
- to: { opacity: 1, y: 0 },
88
97
  duration: 0.6,
89
98
  },
90
99
  {
91
100
  target: '.cards',
92
101
  from: { opacity: 0, y: 20 },
93
- to: { opacity: 1, y: 0 },
94
102
  duration: 0.5,
95
103
  stagger: { each: 0.1, from: 'start' },
96
104
  position: '+=0.1', // starts 0.1s after the previous entry ends
@@ -98,7 +106,6 @@ Motion('intro-sequence', [
98
106
  {
99
107
  target: '.cta',
100
108
  from: { opacity: 0, scale: 0.9 },
101
- to: { opacity: 1, scale: 1 },
102
109
  duration: 0.4,
103
110
  position: '<', // starts at the same time as the previous entry
104
111
  },
@@ -112,6 +119,22 @@ Motion('intro-sequence', [
112
119
  Motion('hero-intro').restart();
113
120
  ```
114
121
 
122
+ ### Object Animation
123
+
124
+ Plain JavaScript objects can be tweened — useful for canvas, audio, WebGL, or any non-DOM state:
125
+
126
+ ```ts
127
+ // Animate plain JS objects (useful for canvas, audio, WebGL)
128
+ const state = { volume: 0, brightness: 100 };
129
+ Motion('audio-fade', state, {
130
+ to: { volume: 1, brightness: 50 },
131
+ duration: 2,
132
+ onUpdate: () => {
133
+ audioNode.gain.value = state.volume;
134
+ },
135
+ }).play();
136
+ ```
137
+
115
138
  ---
116
139
 
117
140
  ## Core Concept
@@ -120,6 +143,90 @@ Every animation in the SDK is a **named timeline**. The name is the first argume
120
143
 
121
144
  Timelines are built declaratively via config objects; there are no `.to()` / `.from()` method calls. Animation state (targets, transforms, styles) is managed internally by the engine.
122
145
 
146
+ If `Motion('name', target, config)` is called when `'name'` already has a timeline, the new entries are **appended** to the existing timeline rather than replacing it. To rebuild from scratch, call `.kill()` first:
147
+
148
+ ```ts
149
+ Motion('hero').kill();
150
+ Motion('hero', '#hero', { from: { opacity: 0 }, duration: 0.8 }).onPageLoad();
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Implicit Values
156
+
157
+ The SDK automatically resolves a missing `from` or `to` by reading the element's **current computed CSS** at build time (i.e. when `Motion()` is first called). This means you rarely need to specify both ends of an animation.
158
+
159
+ ### Three cases
160
+
161
+ | Config | SDK behaviour |
162
+ |--------|--------------|
163
+ | `from` only | Reads current CSS as the `to` target. Animate **from** custom values **into** the element's natural state. |
164
+ | `to` only | Reads current CSS as the `from` starting point. Animate **from** the natural state **to** custom values. |
165
+ | Both | Both endpoints are explicit. Only needed when **neither** endpoint matches the element's natural CSS. |
166
+
167
+ ### Common pattern — reveal animations only need `from`
168
+
169
+ ```ts
170
+ // ❌ Redundant — opacity:1 and y:0 are the element's natural CSS defaults
171
+ Motion('reveal', '.card', {
172
+ from: { opacity: 0, y: 40 },
173
+ to: { opacity: 1, y: 0 },
174
+ duration: 0.6,
175
+ }).onScroll({ scrub: true });
176
+
177
+ // ✅ Correct — SDK reads opacity:1 and y:0 from computed CSS automatically
178
+ Motion('reveal', '.card', {
179
+ from: { opacity: 0, y: 40 },
180
+ duration: 0.6,
181
+ }).onScroll({ scrub: true });
182
+ ```
183
+
184
+ ### When `to` only is correct
185
+
186
+ ```ts
187
+ // Animate FROM the element's current state TO a hover state
188
+ Motion('btn-hover', '.btn', {
189
+ to: { scale: 1.05, backgroundColor: '#0099ff' },
190
+ duration: 0.3,
191
+ }).onHover({ onLeave: 'reverse' });
192
+ ```
193
+
194
+ ### When you need both
195
+
196
+ ```ts
197
+ // Neither endpoint is the element's natural state
198
+ Motion('parallax', '.layer', {
199
+ from: { x: -20, y: -20 },
200
+ to: { x: 20, y: 20 },
201
+ }).onMouseMove({ type: 'axis' });
202
+
203
+ // Animating between two non-default positions
204
+ Motion('swipe', '.panel', {
205
+ from: { x: -100 },
206
+ to: { x: 100 },
207
+ }).onGesture({ types: ['touch'], events: { Left: 'play', Right: 'reverse' } });
208
+ ```
209
+
210
+ ### Natural CSS defaults (common values the SDK resolves automatically)
211
+
212
+ | Property | Natural default |
213
+ |----------|----------------|
214
+ | `opacity` | `1` |
215
+ | `x`, `y`, `z` | `0` |
216
+ | `scale`, `scaleX`, `scaleY` | `1` |
217
+ | `rotate`, `rotateX`, `rotateY` | `0` |
218
+ | `skewX`, `skewY` | `0` |
219
+
220
+ > **Note:** `height: 'auto'` is **not** a natural default for the animation engine — it must be specified explicitly in `to` when needed (e.g. accordion reveals).
221
+
222
+ ### Build-time vs. play-time
223
+
224
+ The SDK reads computed CSS **at build time** (when `Motion()` is called), not at play time. If the element's styles change after the timeline is created, call `.kill()` and rebuild the timeline.
225
+
226
+ ### Edge case — transform cache
227
+
228
+ Transform properties (`x`, `y`, `scale`, `rotate`, etc.) are read from the SDK's **internal transform cache** rather than `getComputedStyle`. This ensures composited transforms remain consistent across animations. Plain CSS properties (`opacity`, `color`, `width`, etc.) are read directly from `getComputedStyle`.
229
+
123
230
  ---
124
231
 
125
232
  ## API Reference
@@ -149,7 +256,7 @@ Calling `Motion()` with the same name on an already-existing timeline returns it
149
256
 
150
257
  | Method | Signature | Description |
151
258
  |--------|-----------|-------------|
152
- | `Motion.set` | `(target: TargetInput, vars: AnimationVars): void` | Immediately apply CSS / transform properties with no animation (like `gsap.set`). Values are written synchronously. |
259
+ | `Motion.set` | `(target: TargetInput, vars: AnimationVars): void` | Immediately apply CSS / transform properties with no animation. Goes through the full animation engine pipeline (including color parsing and transform compositing) but completes in zero time, so applied values persist on the DOM. |
153
260
  | `Motion.get` | `(name: string): Timeline \| undefined` | Get a timeline by name; returns `undefined` if none exists. |
154
261
  | `Motion.has` | `(name: string): boolean` | Check whether a named timeline is registered. |
155
262
  | `Motion.getNames` | `(): string[]` | Return the names of all registered timelines. |
@@ -184,6 +291,13 @@ Motion.reset('.animated-card');
184
291
 
185
292
  GSAP-compatible utility functions accessible via `Motion.utils`. These are drop-in replacements for `gsap.utils.*` helpers.
186
293
 
294
+ `MotionUtils` is also exported directly from the package and can be imported independently:
295
+
296
+ ```ts
297
+ import { MotionUtils } from '@motion.page/sdk';
298
+ // Same object as Motion.utils
299
+ ```
300
+
187
301
  | Method | Signature | Description |
188
302
  |--------|-----------|-------------|
189
303
  | `toArray` | `(target, scope?) → Element[]` | Convert CSS selector, NodeList, HTMLCollection, or Element to a flat array. Drop-in for `gsap.utils.toArray()`. |
@@ -297,9 +411,11 @@ tl.call(
297
411
  | `"-=0.3"` | 0.3 s before the previous entry ends |
298
412
  | `"<"` | At the same start time as the previous entry |
299
413
  | `">"` | Immediately after the previous entry ends |
414
+ | `"<0.2"` | 0.2 s after the start of the previous entry |
415
+ | `">-0.1"` | 0.1 s before the end of the previous entry |
300
416
 
301
417
  ```ts
302
- Motion('demo', '.box', { from: { opacity: 0 }, to: { opacity: 1 }, duration: 1 })
418
+ Motion('demo', '.box', { from: { opacity: 0 }, duration: 1 })
303
419
  .call(() => console.log('halfway'), [], 0.5)
304
420
  .call(() => console.log('done'), [], '>');
305
421
  ```
@@ -320,6 +436,22 @@ tl.clear(): this
320
436
 
321
437
  All trigger methods are chainable and attach behaviour to the timeline without requiring you to manage event listeners manually.
322
438
 
439
+ #### Per-Element Triggers (`each: true`)
440
+
441
+ When targeting multiple elements, `each: true` creates independent per-element timeline instances. Without it, all matched elements share one timeline and play/reverse together.
442
+
443
+ ```ts
444
+ // WITHOUT each — hovering any card plays ALL cards
445
+ Motion('card-hover', '.card', { to: { y: -8 }, duration: 0.3 })
446
+ .onHover({ onLeave: 'reverse' });
447
+
448
+ // WITH each — each card animates independently
449
+ Motion('card-hover', '.card', { to: { y: -8 }, duration: 0.3 })
450
+ .onHover({ each: true, onLeave: 'reverse' });
451
+ ```
452
+
453
+ `each` is supported by `.onHover()`, `.onClick()`, `.onScroll()`, `.onMouseMove()`, and `.onGesture()`.
454
+
323
455
  #### `.onHover(config?)`
324
456
 
325
457
  Play on `mouseenter`, react on `mouseleave`.
@@ -358,7 +490,7 @@ interface ClickConfig {
358
490
  ```ts
359
491
  Motion('menu-toggle', '#menu', {
360
492
  from: { height: 0, opacity: 0 },
361
- to: { height: 'auto', opacity: 1 },
493
+ to: { height: 'auto' }, // height: 'auto' must be explicit
362
494
  duration: 0.4,
363
495
  ease: 'power2.inOut',
364
496
  }).onClick({ target: '#menu-btn', toggle: 'reverse' });
@@ -375,12 +507,55 @@ interface ScrollConfig {
375
507
  end?: string; // e.g. 'bottom 20%'
376
508
  scrub?: boolean | number; // true = instant, number = smoothing seconds
377
509
  snap?: number | number[] | ((progress: number) => number); // Snap scroll progress
378
- markers?: boolean | MarkerConfig; // Debug markers
510
+ markers?: boolean | MarkerConfig; // Debug markers (pass object for styling)
379
511
  scroller?: string | Element; // Custom scroll container
380
- pin?: boolean | string; // Pin the element during scroll
512
+ pin?: boolean | string; // true = pin animation target; string = pin a different element
381
513
  pinSpacing?: boolean | 'margin' | 'padding';
382
514
  each?: boolean;
383
- toggleActions?: string; // e.g. 'play none none reverse'
515
+ toggleActions?: string; // Format: 'onEnter onLeave onEnterBack onLeaveBack'
516
+ }
517
+ ```
518
+
519
+ **`start` / `end` defaults:**
520
+ - Without `pin`: `start: 'top bottom'`, `end: 'bottom top'`
521
+ - With `pin`: `start: 'top top'`, `end: 'bottom top'`
522
+
523
+ **Relative `end` with `+=`:** When `end` starts with `+=`, the value is a distance measured from the `start` position:
524
+
525
+ ```ts
526
+ .onScroll({ start: 'top center', end: '+=800' }) // 800px of scroll travel
527
+ .onScroll({ start: 'top top', end: '+=100vh' }) // one viewport height of scroll
528
+ ```
529
+
530
+ **`pin: string`** pins a different element (e.g. a parent wrapper) while the animated child scrolls:
531
+
532
+ ```ts
533
+ // Pin the parent section while the child content animates
534
+ Motion('content-reveal', '.content', {
535
+ from: { opacity: 0, y: 40 },
536
+ duration: 1,
537
+ }).onScroll({ scrub: true, pin: '.section-wrapper', start: 'top top', end: '+=600' });
538
+ ```
539
+
540
+ **`toggleActions`** controls what happens at each scroll boundary. Format: `"onEnter onLeave onEnterBack onLeaveBack"`. Default: `"play reverse play reverse"`. Valid actions: `play`, `pause`, `resume`, `reverse`, `restart`, `reset`, `complete`, `none`.
541
+
542
+ ```ts
543
+ // Play once — never reverse (common for reveal animations)
544
+ .onScroll({ toggleActions: 'play none none none' });
545
+
546
+ // Re-animate every time it enters the viewport
547
+ .onScroll({ toggleActions: 'restart none none reset' });
548
+ ```
549
+
550
+ **`markers`** accepts `true` for default debug markers, or a `MarkerConfig` object for custom styling:
551
+
552
+ ```ts
553
+ interface MarkerConfig {
554
+ startColor?: string; // Default: 'green'
555
+ endColor?: string; // Default: 'red'
556
+ fontSize?: string; // e.g. '12px'
557
+ fontWeight?: string;
558
+ indent?: number; // Horizontal offset in pixels
384
559
  }
385
560
  ```
386
561
 
@@ -408,8 +583,7 @@ Motion('h-scroll', '.panel', {
408
583
 
409
584
  ```ts
410
585
  Motion('parallax', '.hero-bg', {
411
- from: { y: 0 },
412
- to: { y: -100 },
586
+ to: { y: -100 },
413
587
  }).onScroll({ scrub: 1, start: 'top top', end: 'bottom top' });
414
588
  ```
415
589
 
@@ -428,6 +602,15 @@ interface MouseMoveConfig {
428
602
  }
429
603
  ```
430
604
 
605
+ **Defaults:**
606
+
607
+ | Option | Default | Notes |
608
+ |--------|---------|-------|
609
+ | `type` | `'distance'` | |
610
+ | `startProgress` | `0.5` | Progress value when mouse is at rest |
611
+ | `leaveProgress` | `0.5` | Progress to animate to on mouse leave |
612
+ | `smooth` | `0.1` | `0` = instant tracking, `1` = maximum lag |
613
+
431
614
  ```ts
432
615
  Motion('parallax-depth', '.layer', {
433
616
  from: { x: -20, y: -20 },
@@ -438,15 +621,54 @@ Motion('parallax-depth', '.layer', {
438
621
 
439
622
  #### `.onPageLoad()`
440
623
 
441
- Play the animation automatically when the page finishes loading.
624
+ Play the animation automatically when the page finishes loading. If called after `DOMContentLoaded` has already fired (common in SPAs or scripts placed at the bottom of `<body>`), the animation plays immediately.
442
625
 
443
626
  ```ts
444
627
  Motion('page-intro', [
445
- { target: '.logo', from: { opacity: 0 }, to: { opacity: 1 }, duration: 0.5 },
446
- { target: '.nav', from: { y: -20, opacity: 0 }, to: { y: 0, opacity: 1 }, duration: 0.4 },
628
+ { target: '.logo', from: { opacity: 0 }, duration: 0.5 },
629
+ { target: '.nav', from: { y: -20, opacity: 0 }, duration: 0.4 },
447
630
  ]).onPageLoad();
448
631
  ```
449
632
 
633
+ #### `.onPageExit(config?)`
634
+
635
+ Intercept link clicks, play the exit animation, then navigate to the destination URL after the timeline completes. Works on any website with no server-side dependencies.
636
+
637
+ ```ts
638
+ interface PageExitConfig {
639
+ /** 'all' (default) | 'include' | 'exclude' */
640
+ mode?: 'all' | 'include' | 'exclude';
641
+ /** CSS selectors for links — required when mode is 'include' or 'exclude' */
642
+ selectors?: string;
643
+ /** Href patterns to skip automatically. Note: 'mailto' also skips tel: links. */
644
+ skipHref?: ('anchor' | 'javascript' | 'mailto')[];
645
+ }
646
+ ```
647
+
648
+ - `mode: 'all'` (default) — intercepts every `<a>` on the page
649
+ - `mode: 'include'` — only links matching `selectors`
650
+ - `mode: 'exclude'` — all links except those matching `selectors`
651
+ - Automatically skips `target="_blank"` links and modifier-key clicks (Cmd/Ctrl/Shift/Alt)
652
+
653
+ ```ts
654
+ // Fade-out on page exit — all links
655
+ Motion('page-exit', 'body', {
656
+ to: { opacity: 0 },
657
+ duration: 0.4,
658
+ ease: 'power2.in',
659
+ }).onPageExit();
660
+
661
+ // Only internal nav links
662
+ Motion('page-exit', 'body', {
663
+ to: { opacity: 0 },
664
+ duration: 0.4,
665
+ }).onPageExit({
666
+ mode: 'include',
667
+ selectors: 'nav a',
668
+ skipHref: ['anchor', 'mailto'],
669
+ });
670
+ ```
671
+
450
672
  #### `.onGesture(config)`
451
673
 
452
674
  Respond to pointer, touch, wheel, or scroll gestures with fine-grained event-to-action mapping.
@@ -484,10 +706,36 @@ interface GestureConfig {
484
706
  }
485
707
  ```
486
708
 
709
+ **Config defaults:**
710
+
711
+ | Option | Default | Unit | Notes |
712
+ |--------|---------|------|-------|
713
+ | `tolerance` | `1` | px | Min movement before direction events fire |
714
+ | `dragMinimum` | `10` | px | Distance before `Drag` fires |
715
+ | `wheelSpeed` | `1` | multiplier | Scales wheel delta |
716
+ | `scrollSpeed` | `1` | multiplier | Scales scroll delta |
717
+ | `stopDelay` | `150` | ms | Idle time after movement before `Stop` fires |
718
+ | `smooth` | `0` | 0–1 | Smoothness for `progressUp`/`progressDown` actions |
719
+ | `animationStep` | `0.1` | 0–1 | Progress step per event for `progressUp`/`progressDown` |
720
+
721
+ **Event distinctions:**
722
+
723
+ - `Up`/`Down`/`Left`/`Right` — fire **continuously** during movement
724
+ - `UpComplete`/`DownComplete`/etc. — fire **once on release** if that direction was active
725
+ - `PressInit` — fires immediately on press, before start position is recorded
726
+ - `Press` — fires after start position is recorded
727
+ - `Hover`/`HoverEnd` — require a `target` element (not the window)
728
+ - `playNext`/`playPrevious` actions — only work when `each: true` is set
729
+
730
+ `animationStep` can be a single number or a per-event map:
731
+
732
+ ```ts
733
+ animationStep: { Up: 0.2, Down: 0.1 } // different step size per direction
734
+ ```
735
+
487
736
  ```ts
488
737
  Motion('swipe-gallery', '.gallery', {
489
- from: { x: 0 },
490
- to: { x: -100 },
738
+ to: { x: -100 },
491
739
  }).onGesture({
492
740
  types: ['pointer', 'touch'],
493
741
  events: {
@@ -518,6 +766,18 @@ interface CursorConfig {
518
766
  }
519
767
  ```
520
768
 
769
+ **`CursorStateVars`** is the shape used for `default`, `hover`, and `click`. The `targets` field controls which elements trigger that state:
770
+
771
+ ```ts
772
+ interface CursorStateVars {
773
+ targets?: string[]; // CSS selectors that trigger this state (e.g. ['a', 'button', '.btn'])
774
+ duration?: number; // Transition duration in seconds (default: 0.15)
775
+ ease?: string; // Easing (default: 'power3.inOut')
776
+ enabled?: boolean; // Whether state is active (default: true)
777
+ [key: string]: any; // Any CSS property: width, height, backgroundColor, scale, etc.
778
+ }
779
+ ```
780
+
521
781
  ```ts
522
782
  Motion('custom-cursor', 'body', {
523
783
  to: { opacity: 1 },
@@ -527,11 +787,52 @@ Motion('custom-cursor', 'body', {
527
787
  smooth: 0.08,
528
788
  hideNative: true,
529
789
  default: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#fff' },
530
- hover: { width: 40, height: 40, backgroundColor: 'transparent', borderColor: '#fff' },
531
- click: { scale: 0.8 },
790
+ hover: {
791
+ targets: ['a', 'button', '[data-cursor-hover]'],
792
+ width: 40, height: 40,
793
+ backgroundColor: 'transparent',
794
+ borderColor: '#fff',
795
+ },
796
+ click: { scale: 0.8 },
797
+ });
798
+ ```
799
+
800
+ **`type: 'text'`** — reads text from `mp-cursor-text` or `mp-cursor-tooltip` HTML attributes and displays it inside the cursor element. The `text` config object sets CSS properties on the text node:
801
+
802
+ ```ts
803
+ Motion('cursor', 'body', { to: { opacity: 1 }, duration: 0 }).onCursor({
804
+ target: '#cursor',
805
+ type: 'text',
806
+ hideNative: true,
807
+ default: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#fff' },
808
+ hover: { width: 64, height: 64 },
809
+ text: { fontSize: '12px', color: '#000', fontWeight: 'bold' },
810
+ });
811
+ ```
812
+
813
+ ```html
814
+ <a href="/about" mp-cursor-text="About Us">About</a>
815
+ <button mp-cursor-tooltip="Click me">Button</button>
816
+ ```
817
+
818
+ **`type: 'media'`** — reads an image or video URL from the `mp-cursor-media` attribute and renders it inside the cursor. Supports http/https and relative URLs. The `media` config object sets CSS on the media element:
819
+
820
+ ```ts
821
+ Motion('cursor', 'body', { to: { opacity: 1 }, duration: 0 }).onCursor({
822
+ target: '#cursor',
823
+ type: 'media',
824
+ hideNative: true,
825
+ default: { width: 48, height: 48, borderRadius: '50%' },
826
+ hover: { width: 120, height: 80 },
827
+ media: { borderRadius: '8px', objectFit: 'cover' },
532
828
  });
533
829
  ```
534
830
 
831
+ ```html
832
+ <div class="project-card" mp-cursor-media="/images/preview.jpg">…</div>
833
+ <div class="video-card" mp-cursor-media="https://cdn.example.com/preview.mp4">…</div>
834
+ ```
835
+
535
836
  ---
536
837
 
537
838
  ### Lifecycle Callbacks
@@ -543,7 +844,6 @@ Attach callbacks via the `AnimationConfig` object or directly on the `Timeline`
543
844
  ```ts
544
845
  Motion('slide-in', '.card', {
545
846
  from: { opacity: 0, x: -40 },
546
- to: { opacity: 1, x: 0 },
547
847
  duration: 0.6,
548
848
  onStart: () => console.log('started'),
549
849
  onUpdate: (progress) => console.log('progress:', progress),
@@ -557,7 +857,7 @@ Motion('slide-in', '.card', {
557
857
  #### Via `Timeline` Methods
558
858
 
559
859
  ```ts
560
- Motion('slide-in', '.card', { from: { opacity: 0 }, to: { opacity: 1 }, duration: 0.6 })
860
+ Motion('slide-in', '.card', { from: { opacity: 0 }, duration: 0.6 })
561
861
  .onStart(() => console.log('started'))
562
862
  .onUpdate((progress, time) => console.log(progress, time))
563
863
  .onComplete(() => console.log('done'));
@@ -585,8 +885,10 @@ interface AnimationConfig {
585
885
  delay?: number; // Seconds before animation begins
586
886
  ease?: string; // Easing name string, see Easing section
587
887
  stagger?: number | StaggerVars; // Per-element stagger delay
588
- repeat?: RepeatConfig; // Loop/yoyo configuration
888
+ repeat?: number | RepeatConfig; // Repeat count shorthand or full config (see below)
589
889
  split?: SplitType; // Text splitting for per-char/word/line animation
890
+ mask?: boolean; // Wrap split elements in overflow:hidden for reveal effects
891
+ fit?: FitConfig; // FLIP-style morph toward another element
590
892
  axis?: 'x' | 'y'; // Axis binding for onMouseMove animations
591
893
 
592
894
  // Lifecycle
@@ -638,13 +940,68 @@ drawSVG // string | { start?: number; end?: n
638
940
 
639
941
  // Motion path
640
942
  path: {
641
- target: string | Element; // SVG <path> selector or element
943
+ target: string | Element; // SVG <path> selector, element, or raw path data (starts with M/m)
642
944
  align?: string | Element; // Align bounding box to this element
643
945
  alignAt?: [number, number]; // Origin point [x%, y%], default [50, 50]
644
946
  start?: number; // Path start (0–1), default 0
645
947
  end?: number; // Path end (0–1), default 1
646
948
  rotate?: boolean; // Auto-rotate along tangent
647
949
  }
950
+
951
+ // CSS custom properties
952
+ '--my-var' // any CSS custom property name (string)
953
+ ```
954
+
955
+ ### drawSVG
956
+
957
+ Animate the visible portion of an SVG stroke. The element must have a `stroke` and a set `stroke-dasharray` (or the SDK will compute it automatically).
958
+
959
+ | Format | Meaning |
960
+ |--------|---------|
961
+ | `"0% 100%"` | Full stroke visible |
962
+ | `"0% 0%"` | Stroke fully hidden (start position for a draw-in) |
963
+ | `"20% 80%"` | Middle portion only |
964
+ | `"50%"` | Shorthand for `"0% 50%"` |
965
+ | `"100px 500px"` | Pixel range along the stroke |
966
+ | `{ start: 20, end: 80 }` | Object form — values are **percentages 0–100**, not 0–1 |
967
+
968
+ ```ts
969
+ // Animate stroke from hidden to fully drawn
970
+ Motion('draw-path', 'path#line', {
971
+ from: { drawSVG: '0% 0%' },
972
+ to: { drawSVG: '0% 100%' },
973
+ duration: 1.2,
974
+ ease: 'power2.inOut',
975
+ }).onPageLoad();
976
+
977
+ // Object format — percentages, not 0–1
978
+ Motion('draw-partial', '#circle', {
979
+ to: { drawSVG: { start: 20, end: 80 } },
980
+ duration: 0.8,
981
+ }).play();
982
+ ```
983
+
984
+ ### path
985
+
986
+ `path.target` accepts a CSS selector, an `Element`, or raw SVG path data (a string starting with `M` or `m`):
987
+
988
+ ```ts
989
+ // Inline path data — no DOM element required
990
+ Motion('fly', '.icon', {
991
+ to: { path: { target: 'M 0 100 C 50 0 150 200 200 100', rotate: true } },
992
+ duration: 2,
993
+ }).play();
994
+ ```
995
+
996
+ ### CSS Custom Properties
997
+
998
+ CSS custom properties can be animated by passing the property name as a string key:
999
+
1000
+ ```ts
1001
+ Motion('theme', ':root', {
1002
+ to: { '--primary-hue': 240, '--accent-opacity': 0.8 },
1003
+ duration: 0.5,
1004
+ }).onClick({ target: '#theme-btn' });
648
1005
  ```
649
1006
 
650
1007
  ### StaggerVars
@@ -662,12 +1019,23 @@ interface StaggerVars {
662
1019
 
663
1020
  ### RepeatConfig
664
1021
 
1022
+ The `repeat` field accepts either a plain number or a `RepeatConfig` object:
1023
+
665
1024
  ```ts
1025
+ // Shorthand — number of additional repetitions
1026
+ repeat: 3 // repeat 3 more times after the first play
1027
+ repeat: -1 // repeat infinitely
1028
+
1029
+ // Full config
666
1030
  interface RepeatConfig {
667
1031
  times: number; // Number of additional repetitions (-1 = infinite)
668
1032
  delay?: number; // Seconds between repetitions
669
1033
  yoyo?: boolean; // Alternate direction each cycle
670
1034
  }
1035
+
1036
+ // Examples
1037
+ repeat: { times: -1, yoyo: true, delay: 0.2 } // infinite yoyo with pause between cycles
1038
+ repeat: { times: 2, yoyo: true, delay: 0.5 } // 2 extra cycles, yoyo, 0.5s pause
671
1039
  ```
672
1040
 
673
1041
  ### SplitType
@@ -682,18 +1050,67 @@ type SplitType =
682
1050
  | 'chars,words,lines';
683
1051
  ```
684
1052
 
685
- Text is split into wrapper `<span>` elements before animating. `Motion.reset()` reverts the DOM.
1053
+ Text is split into wrapper `<span>` elements before animating. `Motion.reset()` reverts the DOM. Inline elements (like `<span class="accent">`) are preserved during splitting.
1054
+
1055
+ Split elements receive data attributes for CSS targeting:
1056
+
1057
+ | Attribute | Set on | Index attribute |
1058
+ |-----------|--------|-----------------|
1059
+ | `[data-split-char]` | each character span | `data-char-index` |
1060
+ | `[data-split-word]` | each word span | `data-word-index` |
1061
+ | `[data-split-line]` | each line span | `data-line-index` |
1062
+ | `[data-split-mask]` | overflow wrapper (when `mask: true`) | — |
686
1063
 
687
1064
  ```ts
688
1065
  Motion('text-reveal', '.headline', {
689
1066
  from: { opacity: 0, y: 20 },
690
- to: { opacity: 1, y: 0 },
691
1067
  duration: 0.5,
692
1068
  split: 'chars',
693
1069
  stagger: { each: 0.03, from: 'start' },
694
1070
  }).onPageLoad();
695
1071
  ```
696
1072
 
1073
+ ### mask
1074
+
1075
+ When `mask: true` is used together with `split`, each split element (char, word, or line) is wrapped in a parent with `overflow: hidden`. This clips animated content to its natural bounds, creating a 'reveal' effect — for example, animating `y: '100%'` makes text slide up from behind an invisible edge.
1076
+
1077
+ ```ts
1078
+ Motion('reveal', 'h1', {
1079
+ split: 'lines',
1080
+ mask: true,
1081
+ from: { y: '100%' },
1082
+ stagger: 0.1,
1083
+ }).onPageLoad();
1084
+ ```
1085
+
1086
+ ### FitConfig
1087
+
1088
+ FLIP-style morph animation. When `fit` is set, `from` and `to` are ignored — the SDK measures both elements' bounding rects at play time and animates the visual delta between them.
1089
+
1090
+ ```ts
1091
+ interface FitConfig {
1092
+ /** CSS selector for the target element to morph toward */
1093
+ target: string;
1094
+ /** Convert to absolute positioning during animation. Default: false */
1095
+ absolute?: boolean;
1096
+ /** Include scale changes. Default: true */
1097
+ scale?: boolean;
1098
+ /** Animate actual width/height instead of scaleX/scaleY. Default: false */
1099
+ resize?: boolean;
1100
+ }
1101
+ ```
1102
+
1103
+ - **`scale: true`** (default) — animates using `scaleX`/`scaleY`. Fast, GPU-accelerated, but can distort text, borders, and box-shadows.
1104
+ - **`resize: true`** — animates actual `width`/`height` properties instead. No visual distortion, but triggers layout reflow. Mutually exclusive with `scale`.
1105
+
1106
+ ```ts
1107
+ Motion('reorder', '.container', {
1108
+ fit: { target: '.item', resize: true },
1109
+ duration: 0.5,
1110
+ ease: 'power2.inOut',
1111
+ }).play();
1112
+ ```
1113
+
697
1114
  ---
698
1115
 
699
1116
  ## Easing
@@ -703,23 +1120,22 @@ Easing names are **case-insensitive** strings. Pass them to `AnimationConfig.eas
703
1120
  | Family | Variants |
704
1121
  |--------|----------|
705
1122
  | `linear`, `none` | — |
706
- | `power1` | `power1.in` · `power1.out` · `power1.inout` |
707
- | `power2` | `power2.in` · `power2.out` · `power2.inout` |
708
- | `power3` | `power3.in` · `power3.out` · `power3.inout` |
709
- | `power4` | `power4.in` · `power4.out` · `power4.inout` |
710
- | `sine` | `sine.in` · `sine.out` · `sine.inout` |
711
- | `expo` | `expo.in` · `expo.out` · `expo.inout` |
712
- | `circ` | `circ.in` · `circ.out` · `circ.inout` |
713
- | `back` | `back.in` · `back.out` · `back.inout` |
714
- | `elastic` | `elastic.in` · `elastic.out` · `elastic.inout` |
715
- | `bounce` | `bounce.in` · `bounce.out` · `bounce.inout` |
1123
+ | `power1` | `power1.in` · `power1.out` · `power1.inOut` |
1124
+ | `power2` | `power2.in` · `power2.out` · `power2.inOut` |
1125
+ | `power3` | `power3.in` · `power3.out` · `power3.inOut` |
1126
+ | `power4` | `power4.in` · `power4.out` · `power4.inOut` |
1127
+ | `sine` | `sine.in` · `sine.out` · `sine.inOut` |
1128
+ | `expo` | `expo.in` · `expo.out` · `expo.inOut` |
1129
+ | `circ` | `circ.in` · `circ.out` · `circ.inOut` |
1130
+ | `back` | `back.in` · `back.out` · `back.inOut` |
1131
+ | `elastic` | `elastic.in` · `elastic.out` · `elastic.inOut` |
1132
+ | `bounce` | `bounce.in` · `bounce.out` · `bounce.inOut` |
716
1133
 
717
1134
  Unknown strings fall back to `power1.out`.
718
1135
 
719
1136
  ```ts
720
1137
  Motion('spring-in', '.box', {
721
1138
  from: { scale: 0 },
722
- to: { scale: 1 },
723
1139
  duration: 0.8,
724
1140
  ease: 'elastic.out',
725
1141
  }).play();
@@ -731,6 +1147,8 @@ Motion('spring-in', '.box', {
731
1147
 
732
1148
  All types are re-exported from the package entry point.
733
1149
 
1150
+ Most users won't need to import types directly — TypeScript infers everything from the `Motion()` function signature, so you get full autocomplete and type checking out of the box. These exports are provided for advanced use cases like building wrapper libraries or typing standalone config objects.
1151
+
734
1152
  ```ts
735
1153
  import type {
736
1154
  // Core
@@ -747,6 +1165,7 @@ import type {
747
1165
  RepeatConfig,
748
1166
  SplitType,
749
1167
  PathConfig,
1168
+ FitConfig,
750
1169
 
751
1170
  // Triggers
752
1171
  HoverConfig,
@@ -754,6 +1173,7 @@ import type {
754
1173
  ScrollConfig,
755
1174
  MouseMoveConfig,
756
1175
  MarkerConfig,
1176
+ PageExitConfig,
757
1177
 
758
1178
  // Gesture
759
1179
  GestureConfig,
@@ -765,38 +1185,38 @@ import type {
765
1185
  CursorConfig,
766
1186
  CursorStateVars,
767
1187
  CursorSqueezeConfig,
768
- } from '@motion/sdk';
1188
+ } from '@motion.page/sdk';
769
1189
 
770
1190
  // Namespace import
771
- import { Types } from '@motion/sdk';
1191
+ import { Types } from '@motion.page/sdk';
772
1192
  ```
773
1193
 
774
1194
  ---
775
1195
 
776
1196
  ## Browser Build
777
1197
 
778
- A pre-built IIFE bundle is generated at `dist/motion-sdk.browser.js` for direct `<script>` tag usage:
1198
+ ### Browser Build (IIFE)
1199
+
1200
+ An IIFE build script is included but not part of the default build output. To generate a browser bundle:
1201
+
1202
+ ```bash
1203
+ bun run packages/sdk/scripts/build-iife.ts
1204
+ ```
1205
+
1206
+ This creates a self-contained script that exposes `window.Motion` and `window.MotionTimeline`:
779
1207
 
780
1208
  ```html
781
1209
  <script src="motion-sdk.browser.js"></script>
782
1210
  <script>
783
- // Globals are exposed on window:
784
1211
  const { Motion, MotionTimeline } = window;
785
1212
 
786
- Motion('fade-in', '.hero', {
787
- from: { opacity: 0 },
788
- to: { opacity: 1 },
789
- duration: 1,
1213
+ Motion('fade', '.hero', {
1214
+ from: { opacity: 0, y: 30 },
1215
+ duration: 0.8,
790
1216
  }).onPageLoad();
791
1217
  </script>
792
1218
  ```
793
1219
 
794
- To regenerate the browser bundle:
795
-
796
- ```bash
797
- bun run packages/sdk/scripts/build-iife.ts
798
- ```
799
-
800
1220
  ---
801
1221
 
802
1222
  ## Browser Support
@@ -814,4 +1234,12 @@ Modern evergreen browsers:
814
1234
 
815
1235
  ## License
816
1236
 
817
- Proprietarysee [LICENSE](../../LICENSE) for details.
1237
+ **FSL-1.1-Apache-2.0**[Functional Source License, Version 1.1, Apache 2.0 Future License](./LICENSE)
1238
+
1239
+ **TL;DR:** Free for everyone. Use it in your websites, apps, SaaS products, client projects — commercial or not. No restrictions.
1240
+
1241
+ **The one exception:** You cannot use this SDK to build a product that competes with Motion.page (e.g., a no-code animation builder, visual animation editor, or similar tool). If you're building something like that, [contact us](mailto:hello@motion.page) for an enterprise license.
1242
+
1243
+ After 2 years from each release, the code converts to the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license with no restrictions at all.
1244
+
1245
+ See [LICENSE](./LICENSE) for the full legal text.