@cosmictraveler002/anim-kit 1.2.0 → 1.4.0

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.
Files changed (44) hide show
  1. package/README.md +275 -31
  2. package/dist/anim-kit.standalone.js +5 -5
  3. package/dist/anim-kit.standalone.js.map +1 -1
  4. package/dist/core/gsap.d.ts +1 -1
  5. package/dist/core/gsap.d.ts.map +1 -1
  6. package/dist/core/gsap.js +1 -1
  7. package/dist/core/gsap.js.map +1 -1
  8. package/dist/core/smooth-scroll.d.ts +1 -1
  9. package/dist/core/smooth-scroll.js +1 -1
  10. package/dist/effects/drag-rail.d.ts +31 -0
  11. package/dist/effects/drag-rail.d.ts.map +1 -0
  12. package/dist/effects/drag-rail.js +235 -0
  13. package/dist/effects/drag-rail.js.map +1 -0
  14. package/dist/effects/preloader.js +1 -1
  15. package/dist/effects/reel-text.d.ts +19 -0
  16. package/dist/effects/reel-text.d.ts.map +1 -0
  17. package/dist/effects/reel-text.js +163 -0
  18. package/dist/effects/reel-text.js.map +1 -0
  19. package/dist/index.d.ts +7 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/three/gl-rail.d.ts +29 -0
  24. package/dist/three/gl-rail.d.ts.map +1 -0
  25. package/dist/three/gl-rail.js +350 -0
  26. package/dist/three/gl-rail.js.map +1 -0
  27. package/dist/three/index.d.ts +15 -0
  28. package/dist/three/index.d.ts.map +1 -0
  29. package/dist/three/index.js +13 -0
  30. package/dist/three/index.js.map +1 -0
  31. package/dist/three/webgl-media.d.ts +21 -0
  32. package/dist/three/webgl-media.d.ts.map +1 -0
  33. package/dist/three/webgl-media.js +268 -0
  34. package/dist/three/webgl-media.js.map +1 -0
  35. package/package.json +17 -1
  36. package/src/core/gsap.ts +1 -1
  37. package/src/core/smooth-scroll.ts +1 -1
  38. package/src/effects/drag-rail.ts +285 -0
  39. package/src/effects/preloader.ts +1 -1
  40. package/src/effects/reel-text.ts +212 -0
  41. package/src/index.ts +9 -1
  42. package/src/three/gl-rail.ts +437 -0
  43. package/src/three/index.ts +14 -0
  44. package/src/three/webgl-media.ts +325 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # anim-kit
2
2
 
3
- A modular, framework-agnostic animation library extracted from
4
- [dzinrstudio.com](https://dzinrstudio.com/) — GSAP + ScrollTrigger + Lenis
3
+ A modular, framework-agnostic animation library by
4
+ [kalakritico.in](https://kalakritico.in/) — GSAP + ScrollTrigger + Lenis
5
5
  effects packaged as independent ES modules.
6
6
 
7
7
  TypeScript source → compiled ESM + `.d.ts` output. No framework, no virtual DOM,
@@ -78,8 +78,9 @@ inside the published tarball):
78
78
 
79
79
  | Specifier | Resolves to | Use for |
80
80
  |---|---|---|
81
- | `@cosmictraveler002/anim-kit` | `dist/index.js` + `dist/index.d.ts` | the full barrel — 45 exports |
81
+ | `@cosmictraveler002/anim-kit` | `dist/index.js` + `dist/index.d.ts` | the full barrel — 47 exports |
82
82
  | `@cosmictraveler002/anim-kit/effects/<name>` | `dist/effects/<name>.js` + `.d.ts` | one effect in isolation (`marquee`, `lineReveal`, …) |
83
+ | `@cosmictraveler002/anim-kit/three` | `dist/three/index.js` + `.d.ts` | WebGL effects — needs the optional `three` peer (never a core dep) |
83
84
  | `@cosmictraveler002/anim-kit/standalone` | `dist/anim-kit.standalone.js` (types → `index.d.ts`) | the self-contained bundle — same API |
84
85
  | `@cosmictraveler002/anim-kit/styles` | `dist/styles/anim-kit.css` | untouched plain CSS |
85
86
 
@@ -97,7 +98,7 @@ resolves declarations through the same map — no `typesVersions` shim needed.
97
98
 
98
99
  No build step on the consumer's end: `dist/` is served as-is from the npm
99
100
  tarball by any npm CDN. Every URL is **version-pinned** — npm versions are
100
- immutable, so `@cosmictraveler002/anim-kit@1.2.0` always resolves to exactly that build, forever
101
+ immutable, so `@cosmictraveler002/anim-kit@1.4.0` always resolves to exactly that build, forever
101
102
  (only a new version creates a new URL; nothing floats unless you ask for a
102
103
  range).
103
104
 
@@ -108,12 +109,12 @@ plugins anim-kit uses) and `lenis` **inlined** — no import map, one URL, works
108
109
  identically on jsDelivr and unpkg:
109
110
 
110
111
  ```html
111
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.2.0/dist/styles/anim-kit.css" />
112
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/styles/anim-kit.css" />
112
113
 
113
114
  <script type="module">
114
115
  import {
115
116
  smoothScroll, lineReveal, marquee,
116
- } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.2.0/dist/anim-kit.standalone.js";
117
+ } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/anim-kit.standalone.js";
117
118
 
118
119
  smoothScroll();
119
120
  lineReveal("[data-lines]", { mode: "scroll" });
@@ -121,7 +122,7 @@ identically on jsDelivr and unpkg:
121
122
  </script>
122
123
  ```
123
124
 
124
- unpkg serves the same file: `https://unpkg.com/@cosmictraveler002/anim-kit@1.2.0/dist/anim-kit.standalone.js`
125
+ unpkg serves the same file: `https://unpkg.com/@cosmictraveler002/anim-kit@1.4.0/dist/anim-kit.standalone.js`
125
126
 
126
127
  ### Option 2 — jsDelivr `+esm`
127
128
 
@@ -130,7 +131,7 @@ per version):
130
131
 
131
132
  ```html
132
133
  <script type="module">
133
- import { lineReveal } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.2.0/+esm";
134
+ import { lineReveal } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/+esm";
134
135
  </script>
135
136
  ```
136
137
 
@@ -142,12 +143,14 @@ locally, with CDN URLs — and the way to share one GSAP between anim-kit and
142
143
  the rest of your page:
143
144
 
144
145
  ```html
145
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.2.0/dist/styles/anim-kit.css" />
146
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/styles/anim-kit.css" />
146
147
 
147
148
  <script type="importmap">
148
149
  {
149
150
  "imports": {
150
- "@cosmictraveler002/anim-kit": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.2.0/dist/index.js",
151
+ "@cosmictraveler002/anim-kit": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/index.js",
152
+ "@cosmictraveler002/anim-kit/three": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/three/index.js",
153
+ "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
151
154
  "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/index.js",
152
155
  "gsap/ScrollTrigger": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/ScrollTrigger.js",
153
156
  "gsap/SplitText": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/SplitText.js",
@@ -167,12 +170,42 @@ the rest of your page:
167
170
  </script>
168
171
  ```
169
172
 
170
- Swap the host for unpkg (`https://unpkg.com/@cosmictraveler002/anim-kit@1.2.0/dist/index.js`, …) —
173
+ Swap the host for unpkg (`https://unpkg.com/@cosmictraveler002/anim-kit@1.4.0/dist/index.js`, …) —
171
174
  the file layout is identical. GSAP subpaths are listed one by one because
172
175
  import maps match specifiers literally: a trailing-slash prefix map would
173
176
  produce extension-less URLs, which CDNs don't serve. The `gsap`/`lenis` pins
174
177
  match `package-lock.json`.
175
178
 
179
+ ### Version discipline — the pin must match the code
180
+
181
+ The pin is a contract between the URL and the importing code: a bundle older
182
+ than the names your code imports fails at **module instantiation**, before a
183
+ single line runs —
184
+
185
+ ```text
186
+ SyntaxError: The requested module '…/anim-kit@<old-version>/dist/anim-kit.standalone.js'
187
+ does not provide an export named 'clipWipe'
188
+ ```
189
+
190
+ On a page with a preloader that reads as **frozen on the loader**: the script
191
+ that would dismiss it never executed. Whenever a loader won't clear, open the
192
+ console first — an `… does not provide an export named 'X'` error is version
193
+ skew, not an effect bug.
194
+
195
+ Rules that keep it from happening:
196
+
197
+ - **Never hand-copy the demo or hand-edit CDN URLs.** Regenerate the
198
+ deployable copy with `npm run sync:live` — it rewrites every pin from
199
+ `CDN_VERSION` in `scripts/prompts.mjs`, the single source, and
200
+ `demo-smoke` fails CI if `demo/`, `demo_live/` or this README drift from
201
+ the release version.
202
+ - **On a release bump**, change `version` and `CDN_VERSION` together (the
203
+ smoke enforces it); every copy prompt, the docs page and `demo_live/`
204
+ follow automatically.
205
+ - **In your own project**, every CDN URL you ship must carry the *same*
206
+ version — mixing a new JS pin with an old CSS pin (or vice versa) is the
207
+ same bug in slow motion.
208
+
176
209
  ---
177
210
 
178
211
  ## Quick start
@@ -280,15 +313,16 @@ prompt dock, and backs the `category` / `subcategory` fields on
280
313
  | Text animations | Line & mask reveals | `lineReveal`, `maskReveal` |
281
314
  | Text animations | Per-character scatter | `scatterText` |
282
315
  | Text animations | Decode & scramble | `scrambleText` |
283
- | Text animations | Rolling text | `rollText` |
316
+ | Text animations | Rolling text | `rollText`, `reelText` |
284
317
  | Text animations | Counters | `counter` |
285
318
  | Text animations | Layout transfers | `flipWords` |
286
319
  | Scroll & media | Pinned galleries | `horizontalScroll`, `stackedCards`, `stackedCardsPinned` |
287
320
  | Scroll & media | Parallax & depth | `parallax` |
288
321
  | Scroll & media | Heroes & media | `heroShrink`, `mediaSettle` |
289
322
  | Scroll & media | Enter reveals | `revealRule`, `unfoldReveal`, `clipWipe` |
323
+ | WebGL | Shader media | `webglMedia`, `glRail` |
290
324
  | Loops & marquees | Marquees | `marquee` |
291
- | Loops & marquees | Infinite draggables | `dragStrip` |
325
+ | Loops & marquees | Draggables & rails | `dragStrip`, `dragRail` |
292
326
  | Loops & marquees | Equalizers | `audioBars` |
293
327
  | Buttons & links | Liquid fills | `liquidButton` |
294
328
  | Buttons & links | Underlines | `underlineLink` |
@@ -352,7 +386,7 @@ Central reduced-motion gate. If the user prefers reduced motion and
352
386
  ### Text animations
353
387
 
354
388
  Typography in motion — masked lines, rising masks, per-character scatter,
355
- decode reveals, rolling words, tickers, layout transfers.
389
+ decode reveals, rolling words and reels, tickers, layout transfers.
356
390
 
357
391
  #### `lineReveal(target, options?) => destroy`
358
392
 
@@ -481,6 +515,40 @@ rollText("[data-roll-rev]", { direction: "down" }); // walk rows in reverse
481
515
  blocks itself. `destroy()` unwraps the rows, removes the clone and restores
482
516
  every inline style — markup comes back byte-identical.
483
517
 
518
+ #### `reelText(target, options?) => destroy`
519
+
520
+ Per-character odometer roll — the DOM slot-machine decode. Every alphanumeric
521
+ character gets a masked cell holding a vertical strip of same-case ghost
522
+ glyphs; the strip rolls upward and lands on the real text, staggered left to
523
+ right. Spaces and punctuation pass through untouched.
524
+
525
+ ```html
526
+ <p data-reel>Every character rolls into place.</p>
527
+ ```
528
+
529
+ ```ts
530
+ reelText("[data-reel]", { mode: "scroll", frames: 4, stagger: 0.05 }); // roll on enter
531
+ reelText("[data-immediate]"); // roll at init
532
+ ```
533
+
534
+ | Option | Default | Notes |
535
+ | --- | --- | --- |
536
+ | `mode` | `"immediate"` | `"scroll"` rolls on viewport enter instead |
537
+ | `start` | `"top 85%"` | ScrollTrigger start (scroll mode) |
538
+ | `frames` | `4` | ghost glyphs per character before the final one |
539
+ | `duration` | `0.8` | roll time per character, seconds |
540
+ | `stagger` | `0.05` | delay between characters, seconds |
541
+ | `ease` | `"power4.out"` | GSAP ease for the roll |
542
+ | `replay` | `false` | re-arm on every scroll re-entry instead of once |
543
+ | `force` | `false` | run even under `prefers-reduced-motion` |
544
+
545
+ **DOM:** targets must be plain text — the effect snapshots `innerHTML`, wraps
546
+ each character in a measured mask (width taken from the *final* glyph, so the
547
+ line never jitters while spinning) and restores the original markup byte for
548
+ byte when the roll completes and on `destroy()`. Scroll-mode registration
549
+ happens once and is killed on teardown; reduced motion leaves the target
550
+ completely untouched. No stylesheet required — masks are inline.
551
+
484
552
  #### `flipWords(target, { to }, options?) => destroy`
485
553
 
486
554
  Words measured in one layout, moved into another and animated from where
@@ -727,9 +795,125 @@ the entrance.
727
795
 
728
796
  ---
729
797
 
798
+ ### WebGL
799
+
800
+ GPU media — three.js re-renders your images as shader cards. This is the only
801
+ category behind the `@cosmictraveler002/anim-kit/three` subpath: `three` is an
802
+ **optional peer** (see [Subpath exports](#subpath-exports)), so the core
803
+ barrel and the standalone bundle stay three-free.
804
+
805
+ #### `webglMedia(target, options?) => destroy`
806
+
807
+ > From `@cosmictraveler002/anim-kit/three`, not the core barrel.
808
+
809
+ Re-renders a plain `<img>` as a rounded GL plane: hover presses a dent into
810
+ the picture (the sample UV is squeezed toward the cursor, with a fake dome
811
+ lighting term and a chroma split), and the card wipes in bottom-up on reveal.
812
+ Corners are shader-true (rounded-box SDF with a 1px AA edge) and the cover
813
+ crop runs in-shader (`object-fit: cover` maths), so any source aspect fills
814
+ any card aspect without letterboxing.
815
+
816
+ ```html
817
+ <figure data-gl><img src="photo.jpg" alt="" /></figure>
818
+ ```
819
+
820
+ ```ts
821
+ import { webglMedia } from "@cosmictraveler002/anim-kit/three";
822
+
823
+ webglMedia("[data-gl]", { corner: 18, dent: 70, chroma: 2 });
824
+ ```
825
+
826
+ | Option | Default | Notes |
827
+ | --- | --- | --- |
828
+ | `src` | first `<img>` | image source override when the wrapper has none |
829
+ | `corner` | `16` | corner radius, px (SDF, not `border-radius`) |
830
+ | `dent` | `70` | dent depth pressed into the card on hover, px |
831
+ | `chroma` | `2` | rgb split at full hover, px |
832
+ | `reveal` | `true` | wipe the card in bottom-up when its texture loads |
833
+ | `revealDuration` | `1.1` | reveal duration, seconds |
834
+ | `hoverDuration` | `0.6` | hover response duration, seconds |
835
+ | `dpr` | `2` | device-pixel-ratio cap |
836
+ | `force` | `false` | run even under `prefers-reduced-motion` |
837
+
838
+ **Silent no-op ladder.** Missing target → no-op; no `WebGLRenderingContext`
839
+ (SSR, jsdom, WebGL disabled) → no-op *before* any context probe (a probe
840
+ itself logs an error in jsdom); renderer refused → no-op; texture 404 → the
841
+ plain `<img>` stays visible. Nothing ever logs. Reduced motion → the canvas
842
+ never mounts; the static image is the resting state.
843
+
844
+ **DOM.** The wrapper gets inline `position: relative` if it was static (so
845
+ the canvas can cover it — restored on destroy) and keeps layout, sizing and
846
+ alt text in its `<img>`; the canvas takes over only after the texture has
847
+ loaded. `destroy()` kills ticker + tweens + resize observer and disposes
848
+ geometry, material, texture and renderer.
849
+
850
+ #### `glRail(track, options?) => destroy`
851
+
852
+ > From `@cosmictraveler002/anim-kit/three`, not the core barrel.
853
+
854
+ The bounded drag rail drawn as a **WebGL overlay**: one fixed alpha canvas
855
+ covers the stage and re-creates every card's picture from its DOM rect each
856
+ frame, with the surface bent around an invisible cylinder — `dragRail`'s
857
+ physics (intent/pos ticker, tanh rubber-band, throw momentum, Lenis-safe
858
+ wheel) underneath, and a perspective grid floor that fades into the horizon
859
+ and drifts with the rail. The DOM keeps layout, labels and hit areas: the
860
+ canvas paints *under* the track, so a caption sits over the GL media for
861
+ free, and only each `<img>` gets hidden — after its texture has loaded.
862
+
863
+ The bend is one uniform per card in the vertex shader: each vertex maps its
864
+ offset onto the cylinder (`a = o / R`, `x = R·sin a`, `z = −R·(1−cos a)`)
865
+ re-centred on the card's own middle — the centre stays glued to the DOM
866
+ position while the edges foreshorten, and the rotation arrives free from the
867
+ curve. `axis: "auto"` hands the same rule to physics *and* bend: a row
868
+ overflows → horizontal, only the column overflows → vertical, so one init
869
+ serves the desktop row and the mobile stack.
870
+
871
+ ```html
872
+ <div class="rail-screen"> <!-- position: relative; overflow: hidden -->
873
+ <div data-rail>
874
+ <article class="rail-card"><img src="card-01.jpg" alt="" /><p>Night Atlas</p></article>
875
+ <article class="rail-card"><img src="card-02.jpg" alt="" /><p>Signal Bloom</p></article>
876
+ </div>
877
+ </div>
878
+ ```
879
+
880
+ ```ts
881
+ import { glRail } from "@cosmictraveler002/anim-kit/three";
882
+
883
+ glRail("[data-rail]", { radius: 1200, grid: true, throwScale: 14 });
884
+ ```
885
+
886
+ | Option | Default | Notes |
887
+ | --- | --- | --- |
888
+ | `viewport` | track's parent | the stage the canvas covers — keep it `overflow: hidden` |
889
+ | `card` | `":scope > *"` | cards re-rendered — each needs an `<img>` |
890
+ | `axis` | `"auto"` | `"x"` / `"y"` / `"auto"` — picks motion **and** bend axis |
891
+ | `radius` | `1200` | cylinder radius the cards bend around, px |
892
+ | `corner` | `16` | corner radius, px (SDF, not `border-radius`) |
893
+ | `grid` | `true` | perspective grid floor under the cards |
894
+ | `dpr` | `2` | device-pixel-ratio cap |
895
+ | `lerp` / `edge` / `throwScale` / `wheel` / `onTick` | `0.1` / `140` / `14` / `true` / — | `dragRail` physics passthrough |
896
+ | `force` | `false` | run even under `prefers-reduced-motion` |
897
+
898
+ **Silent no-op ladder.** Missing target → no-op; no `WebGLRenderingContext`
899
+ (SSR, jsdom, WebGL disabled) → the **flat `dragRail` rail** still runs
900
+ (plain `<img>`s, full motion) *before* any context probe; renderer refused
901
+ → flat rail; texture 404 → that one card keeps its image; reduced motion →
902
+ nothing mounts. Nothing ever logs. `destroy()` unwinds physics + GPU
903
+ together (listeners, ticker, observers, geometry, materials, textures,
904
+ renderer, canvas) and restores image opacities + the stage's inline
905
+ position.
906
+
907
+ **DOM.** Stage `position: relative; overflow: hidden` with a transparent
908
+ card background; the track is a flex row (or `flex-direction: column; width:
909
+ 100%; height: max-content` for the vertical layout) with `cursor: grab;
910
+ user-select: none`.
911
+
912
+ ---
913
+
730
914
  ### Loops & marquees
731
915
 
732
- Continuous motion — marquees, infinite draggables, equaliser bars.
916
+ Continuous motion — marquees, draggable rails, equaliser bars.
733
917
 
734
918
  #### `marquee(track, options?) => destroy`
735
919
 
@@ -798,6 +982,60 @@ cursor/user-select/touch-action.
798
982
  `touch-action: pan-y` inline and restores them on destroy); items keep
799
983
  `transform-origin: 50% 100%` so they pivot from their base.
800
984
 
985
+ #### `dragRail(track, options?) => destroy`
986
+
987
+ Bounded drag rail with real physics — the finite counterpart to
988
+ `dragStrip()`'s infinite loop. Pointer, wheel and trackpad all feed one
989
+ *intent* value; a single ticker lerps the track toward it (`lerp` scaled by
990
+ GSAP's `deltaRatio`, so the settle rate is framerate-independent), past
991
+ either end the intent squashes through a tanh rubber-band
992
+ (`edge * tanh(overshoot / edge)` — pull further, gain less, never a hard
993
+ stop), and a release coasts on velocity sampled over the last few frames.
994
+ Horizontal by default; `axis: "auto"` flips the rail vertical when only the
995
+ column overflows, so a mobile card stack needs no re-init.
996
+
997
+ ```ts
998
+ dragRail("[data-rail]", { tilt: 0.05, throwScale: 14, wheel: true, axis: "auto" });
999
+ ```
1000
+
1001
+ | Option | Default | Notes |
1002
+ | --- | --- | --- |
1003
+ | `viewport` | track's parent | scroll viewport around the track |
1004
+ | `axis` | `"x"` | `"x"` / `"y"` / `"auto"` — auto follows the layout (`pan-y` / `pan-x` claims the rail's own gesture) |
1005
+ | `item` | `":scope > *"` | children that tilt with velocity |
1006
+ | `lerp` | `0.1` | follow speed toward intent, per 60fps frame (0..1) |
1007
+ | `edge` | `140` | rubber-band resistance distance past the ends, px |
1008
+ | `throwScale` | `14` | momentum multiplier on release |
1009
+ | `wheel` | `true` | wheel/trackpad drives the rail too |
1010
+ | `tilt` | `0` | degrees of tilt per px/frame of velocity (`0` = off) |
1011
+ | `tiltMax` | `8` | tilt clamp, degrees |
1012
+ | `onTick` | — | `(pos, velocity) => {}` every rendered frame |
1013
+ | `force` | `false` | run even under `prefers-reduced-motion` |
1014
+
1015
+ **Single writer.** Pointer, wheel and release handlers only ever touch
1016
+ *intent*; the ticker is the sole writer of the track transform — that
1017
+ separation is what gives a hard throw its buttery settle (the v1 stutter
1018
+ came from letting every input path write `x` directly).
1019
+
1020
+ **Wheel is Lenis-safe.** While the rail can still move in that direction the
1021
+ event gets `preventDefault` **and** `stopPropagation` (Lenis listens above us
1022
+ and would scroll the page in parallel). Once over-extended a full `edge`, the
1023
+ wheel passes through to the page — the section never traps the reader. When
1024
+ input stops, a restore force springs any overscrolled intent home while the
1025
+ lerp chases it.
1026
+
1027
+ **Two axes.** `axis: "auto"` re-resolves on every measure (only the column
1028
+ overflows → vertical) and re-keys the transform on a flip instead of leaving
1029
+ the stale axis behind — resize across the breakpoint and the rail just
1030
+ follows. **vs `glRail`**: the identical physics runs beneath `glRail`'s
1031
+ WebGL overlay (bent cards + grid floor, `/three` subpath) — use plain
1032
+ `dragRail` for DOM media, `glRail` when the pictures should curve.
1033
+
1034
+ **DOM:** same shape as `dragStrip` — `overflow: hidden` viewport, flex track
1035
+ of `width: max-content`; the effect sets inline `cursor` / `user-select` /
1036
+ `touch-action` and clears them plus the transform/tilt on `destroy()`. Feed
1037
+ `onTick(pos, velocity)` a shader or skew effect to drive along with the rail.
1038
+
801
1039
  #### `audioBars(target, options?) => handle`
802
1040
 
803
1041
  Equaliser visualiser — returns `{ start, stop, destroy }`.
@@ -1126,6 +1364,7 @@ destroy on teardown. Route changes and HMR are why `destroy()` exists.
1126
1364
  ```bash
1127
1365
  npm run build # tsc → dist/ (ESM + .d.ts) + CSS copy + tsup standalone bundle
1128
1366
  npm run demo # static server on http://localhost:4321/demo/
1367
+ npm run sync:live # regenerate demo_live/ (static deploy) from demo/
1129
1368
  npm test # build + unit smoke + demo integration smoke
1130
1369
  npm run smoke # both smokes (expects dist/ to exist)
1131
1370
  npm run typecheck # tsc --noEmit (what CI runs)
@@ -1136,18 +1375,24 @@ The demo page wires the whole effect set against one document —
1136
1375
  an on-page *copy prompt* chip (the demo smoke enforces the two-way mapping).
1137
1376
  (`split()` is exercised by the unit smoke instead.)
1138
1377
 
1139
- Two pages share that wiring:
1140
-
1141
- - **`demo/index.html`** (default — `npm run demo` → `/demo/`) — import map on
1142
- local files (`/node_modules/...`, `/dist/index.js`): fast iteration, works
1143
- offline, and the page `demo-smoke` drives.
1144
- - **`demo/anim_kit_live.html`** (`/demo/anim_kit_live.html`) — byte-for-byte
1145
- the same page wired to the **version-pinned CDN** (jsdelivr `@<version>` for
1146
- the package, `gsap@3.15.0`, `lenis@1.3.26`): the exact path visitors copy
1147
- from the prompts and docs, so CDN wiring gets the same manual QA as the
1148
- local build. `demo-smoke` fails if the two files drift apart or the pin
1149
- stops matching the release version, and the page only loads once that
1150
- version is published to npm.
1378
+ Two trees share that wiring:
1379
+
1380
+ - **`demo/`** (default — `npm run demo` → `/demo/`) — import map on local
1381
+ files (`/node_modules/...`, `/dist/index.js`): fast iteration, works
1382
+ offline, and the pages `demo-smoke` drives.
1383
+ - **`demo_live/`** (`npm run sync:live` regenerates it from `demo/`) — a
1384
+ deploy-anywhere copy: the **version-pinned CDN** import map and stylesheet
1385
+ (jsdelivr `@<version>` for the package — the `/three` subpath included —
1386
+ plus `three@0.186.1`, `gsap@3.15.0`, `lenis@1.3.26`: the exact URLs the
1387
+ prompts teach), a byte-copied `assets/` folder, relative internal links, a
1388
+ generated
1389
+ `prompts.json`, and agent crawler files (`robots.txt`, `sitemap.xml`,
1390
+ `llms.txt` with absolute URLs from `SITE_URL`), so the dock and docs run on
1391
+ any static host with no Node server (`/api/prompts` first, `prompts.json`
1392
+ fallback) and agents reach the full catalogue in one fetch instead of
1393
+ scraping HTML. Upload the folder as-is (Vercel, GitHub Pages, Netlify, S3).
1394
+ `demo-smoke` re-runs the generator and fails if the folder goes stale or a
1395
+ pin stops matching the release version.
1151
1396
 
1152
1397
  ### Copy-prompt API
1153
1398
 
@@ -1174,7 +1419,7 @@ and slotting the effect into a subcategory.
1174
1419
  **Unit smoke** (`scripts/smoke.mjs`) runs the built bundle in **jsdom** and
1175
1420
  asserts:
1176
1421
 
1177
- 1. all 45 exports are present;
1422
+ 1. all 47 exports are present;
1178
1423
  2. plugins (`ScrollTrigger`, `SplitText`, `Draggable`, `CustomEase`,
1179
1424
  `Flip`, `ScrollSmoother`) and the 4 custom eases are registered;
1180
1425
  3. every effect no-ops safely on missing targets;
@@ -1219,7 +1464,7 @@ anim-kit/
1219
1464
  │ │ └─ types.ts TargetLike / Destroy / CommonOptions
1220
1465
  │ ├─ effects/ one file per effect (24 files, 28 effect functions)
1221
1466
  │ ├─ styles/anim-kit.css companion stylesheet
1222
- │ └─ index.ts barrel — 45 exports
1467
+ │ └─ index.ts barrel — 47 exports
1223
1468
  ├─ demo/ visual demo (import map, no bundler)
1224
1469
  ├─ scripts/
1225
1470
  │ ├─ serve.mjs static server + /api/prompts (:4321)
@@ -1250,8 +1495,7 @@ Each effect is an independent module — if you only need the marquee, import
1250
1495
 
1251
1496
  ## Credits
1252
1497
 
1253
- Effects extracted and reimplemented from the animation patterns of
1254
- [dzinrstudio.com](https://dzinrstudio.com/). Built on
1498
+ Effects by [kalakritico.in](https://kalakritico.in/). Built on
1255
1499
  [GSAP](https://gsap.com/) (free plugins only) and
1256
1500
  [Lenis](https://lenis.darkroom.engineering/).
1257
1501