use-scroll-animate 5.0.0 → 5.2.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.
@@ -0,0 +1,102 @@
1
+ type MotionSensitivity = 'full' | 'gentle' | 'minimal' | 'static';
2
+ type Cleanup = () => void;
3
+
4
+ /**
5
+ * 5.0 — unified plugin-style effect registration. Every effect (built-in or
6
+ * yours) is a plain object registered once and played the same way:
7
+ * `playEffect(el, name)`, `bindEffect(el, name, { trigger })` or
8
+ * `<usa-fx effect="name" trigger="click">`. Effects get a context that
9
+ * already applies reduced motion, motion sensitivity, intensity and the
10
+ * animation budget.
11
+ */
12
+
13
+ declare const EFFECT_KINDS: readonly ["enter", "exit", "attention", "click", "hover", "card", "loop", "page", "background", "text", "cursor", "scroll"];
14
+ type EffectKind = (typeof EFFECT_KINDS)[number];
15
+ interface EffectContext {
16
+ /** Reduced motion applies (OS setting, `minimal` / `static` sensitivity). */
17
+ readonly reduced: boolean;
18
+ readonly sensitivity: MotionSensitivity;
19
+ /** The triggering event (pointer position for click effects), if any. */
20
+ readonly event?: Event;
21
+ /** `el.animate()` with the library's motion rules (may return `null`). */
22
+ animate(el: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions): Animation | null;
23
+ /** Register teardown for long-running effects (loops, listeners). */
24
+ onCleanup(fn: Cleanup): void;
25
+ }
26
+ interface EffectDefinition<O extends Record<string, unknown> = Record<string, any>> {
27
+ /** Unique, kebab-case. */
28
+ name: string;
29
+ kind: EffectKind;
30
+ /** One line for docs and the gallery. */
31
+ description?: string;
32
+ /** Option defaults (merged under the caller's options). */
33
+ defaults?: Partial<O>;
34
+ /**
35
+ * Under reduced motion: `'skip'` (do nothing — default for loop, background
36
+ * and cursor effects) or `'run'` (run with `ctx.reduced === true`, the
37
+ * effect degrades itself — default for everything else).
38
+ */
39
+ reduced?: 'skip' | 'run';
40
+ /** Play the effect. Return an Animation / Promise to be awaited, or a cleanup. */
41
+ run(el: HTMLElement, options: O, ctx: EffectContext): void | Cleanup | Animation | null | Promise<unknown>;
42
+ }
43
+
44
+ /**
45
+ * 5.1 — card & click effects 2.0, registered through `registerEffect()`.
46
+ * Card: `holo` (holographic foil following the pointer), `glare-sweep`,
47
+ * `book-open`, `card-fan`, `topple`, `float-tilt`. Click: `shockwave`,
48
+ * `ink-splash`, `star-burst`, `jelly-press`, `ring-ripple`, `emoji-rain`.
49
+ */
50
+
51
+ declare const CARD_FX: EffectDefinition[];
52
+ declare const CLICK_FX: EffectDefinition[];
53
+
54
+ /**
55
+ * 5.2 — bounce & physics micro-interactions, registered through
56
+ * `registerEffect()`. Keyframes come from a damped-spring / gravity solver
57
+ * (`springKeyframes()`, `bounceKeyframes()`), so the motion is physical but
58
+ * still runs on the Web Animations API (compositor, reduced-motion aware).
59
+ * Effects: `bounce-in`, `rubber-band`, `elastic-hover`, `drop-bounce`,
60
+ * `gravity-text`, `spring-follow`, `bell-swing`.
61
+ */
62
+
63
+ interface SpringOptions {
64
+ stiffness?: number;
65
+ damping?: number;
66
+ mass?: number;
67
+ /** Samples (keyframes). */
68
+ steps?: number;
69
+ }
70
+ /**
71
+ * Sample a damped spring from 0 → 1 and return the progress values plus the
72
+ * time (ms) it takes to settle. Pure, deterministic.
73
+ */
74
+ declare function solveSpring({ stiffness, damping, mass, steps }?: SpringOptions): {
75
+ values: number[];
76
+ duration: number;
77
+ };
78
+ /** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
79
+ declare function springKeyframes(map: (p: number) => Keyframe, spring?: SpringOptions): {
80
+ frames: Keyframe[];
81
+ duration: number;
82
+ };
83
+ /** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
84
+ declare function bounceKeyframes(bounce?: number, steps?: number): number[];
85
+ declare const PHYSICS_FX: EffectDefinition[];
86
+
87
+ /** Helpers shared by the 5.x effect packs. */
88
+
89
+ /** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
90
+ declare function fxLayer(): HTMLElement;
91
+
92
+ /** The effect packs by version, in release order. */
93
+ declare const EFFECT_PACKS: Record<string, EffectDefinition[]>;
94
+ /** 5.1: card & click effects 2.0. */
95
+ declare function registerCardClickEffects(): void;
96
+ /** 5.2: bounce & physics micro-interactions. */
97
+ declare function registerPhysicsEffects(): void;
98
+ /** Register the built-ins and every pack (idempotent). */
99
+ declare function registerAllEffects(): void;
100
+
101
+ export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
102
+ export type { SpringOptions };
@@ -0,0 +1,102 @@
1
+ type MotionSensitivity = 'full' | 'gentle' | 'minimal' | 'static';
2
+ type Cleanup = () => void;
3
+
4
+ /**
5
+ * 5.0 — unified plugin-style effect registration. Every effect (built-in or
6
+ * yours) is a plain object registered once and played the same way:
7
+ * `playEffect(el, name)`, `bindEffect(el, name, { trigger })` or
8
+ * `<usa-fx effect="name" trigger="click">`. Effects get a context that
9
+ * already applies reduced motion, motion sensitivity, intensity and the
10
+ * animation budget.
11
+ */
12
+
13
+ declare const EFFECT_KINDS: readonly ["enter", "exit", "attention", "click", "hover", "card", "loop", "page", "background", "text", "cursor", "scroll"];
14
+ type EffectKind = (typeof EFFECT_KINDS)[number];
15
+ interface EffectContext {
16
+ /** Reduced motion applies (OS setting, `minimal` / `static` sensitivity). */
17
+ readonly reduced: boolean;
18
+ readonly sensitivity: MotionSensitivity;
19
+ /** The triggering event (pointer position for click effects), if any. */
20
+ readonly event?: Event;
21
+ /** `el.animate()` with the library's motion rules (may return `null`). */
22
+ animate(el: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions): Animation | null;
23
+ /** Register teardown for long-running effects (loops, listeners). */
24
+ onCleanup(fn: Cleanup): void;
25
+ }
26
+ interface EffectDefinition<O extends Record<string, unknown> = Record<string, any>> {
27
+ /** Unique, kebab-case. */
28
+ name: string;
29
+ kind: EffectKind;
30
+ /** One line for docs and the gallery. */
31
+ description?: string;
32
+ /** Option defaults (merged under the caller's options). */
33
+ defaults?: Partial<O>;
34
+ /**
35
+ * Under reduced motion: `'skip'` (do nothing — default for loop, background
36
+ * and cursor effects) or `'run'` (run with `ctx.reduced === true`, the
37
+ * effect degrades itself — default for everything else).
38
+ */
39
+ reduced?: 'skip' | 'run';
40
+ /** Play the effect. Return an Animation / Promise to be awaited, or a cleanup. */
41
+ run(el: HTMLElement, options: O, ctx: EffectContext): void | Cleanup | Animation | null | Promise<unknown>;
42
+ }
43
+
44
+ /**
45
+ * 5.1 — card & click effects 2.0, registered through `registerEffect()`.
46
+ * Card: `holo` (holographic foil following the pointer), `glare-sweep`,
47
+ * `book-open`, `card-fan`, `topple`, `float-tilt`. Click: `shockwave`,
48
+ * `ink-splash`, `star-burst`, `jelly-press`, `ring-ripple`, `emoji-rain`.
49
+ */
50
+
51
+ declare const CARD_FX: EffectDefinition[];
52
+ declare const CLICK_FX: EffectDefinition[];
53
+
54
+ /**
55
+ * 5.2 — bounce & physics micro-interactions, registered through
56
+ * `registerEffect()`. Keyframes come from a damped-spring / gravity solver
57
+ * (`springKeyframes()`, `bounceKeyframes()`), so the motion is physical but
58
+ * still runs on the Web Animations API (compositor, reduced-motion aware).
59
+ * Effects: `bounce-in`, `rubber-band`, `elastic-hover`, `drop-bounce`,
60
+ * `gravity-text`, `spring-follow`, `bell-swing`.
61
+ */
62
+
63
+ interface SpringOptions {
64
+ stiffness?: number;
65
+ damping?: number;
66
+ mass?: number;
67
+ /** Samples (keyframes). */
68
+ steps?: number;
69
+ }
70
+ /**
71
+ * Sample a damped spring from 0 → 1 and return the progress values plus the
72
+ * time (ms) it takes to settle. Pure, deterministic.
73
+ */
74
+ declare function solveSpring({ stiffness, damping, mass, steps }?: SpringOptions): {
75
+ values: number[];
76
+ duration: number;
77
+ };
78
+ /** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
79
+ declare function springKeyframes(map: (p: number) => Keyframe, spring?: SpringOptions): {
80
+ frames: Keyframe[];
81
+ duration: number;
82
+ };
83
+ /** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
84
+ declare function bounceKeyframes(bounce?: number, steps?: number): number[];
85
+ declare const PHYSICS_FX: EffectDefinition[];
86
+
87
+ /** Helpers shared by the 5.x effect packs. */
88
+
89
+ /** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
90
+ declare function fxLayer(): HTMLElement;
91
+
92
+ /** The effect packs by version, in release order. */
93
+ declare const EFFECT_PACKS: Record<string, EffectDefinition[]>;
94
+ /** 5.1: card & click effects 2.0. */
95
+ declare function registerCardClickEffects(): void;
96
+ /** 5.2: bounce & physics micro-interactions. */
97
+ declare function registerPhysicsEffects(): void;
98
+ /** Register the built-ins and every pack (idempotent). */
99
+ declare function registerAllEffects(): void;
100
+
101
+ export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
102
+ export type { SpringOptions };
@@ -0,0 +1,473 @@
1
+ import { registerBuiltinEffects, registerEffects } from './fx.js';
2
+ import '../chunks/base-C3Sw9sAO.js';
3
+ import '../chunks/core-DN3hHbHh.js';
4
+ import './tokens.js';
5
+ import '../chunks/fx-ChjxrMBo.js';
6
+
7
+ let layer = null;
8
+ /** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
9
+ function fxLayer() {
10
+ if (layer?.isConnected)
11
+ return layer;
12
+ layer = document.createElement('div');
13
+ layer.setAttribute('aria-hidden', 'true');
14
+ layer.setAttribute('data-usa-fx-layer', '');
15
+ layer.style.cssText = 'position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:2147483000;contain:strict';
16
+ document.body.appendChild(layer);
17
+ return layer;
18
+ }
19
+ /** Client point of the triggering pointer event, or the element's center. */
20
+ function origin(el, ctx) {
21
+ const e = ctx.event;
22
+ const r = el.getBoundingClientRect();
23
+ return e && typeof e.clientX === 'number' && (e.clientX || e.clientY) ? { x: e.clientX, y: e.clientY } : { x: r.left + r.width / 2, y: r.top + r.height / 2 };
24
+ }
25
+ /** Spawn an absolutely positioned node in the fx layer at (x, y); removed when its animation ends. */
26
+ function spawn(x, y, css, ctx, frames, opts, text = '') {
27
+ const n = document.createElement('span');
28
+ n.style.cssText = `position:absolute;left:${x}px;top:${y}px;${css}`;
29
+ if (text)
30
+ n.textContent = text;
31
+ fxLayer().appendChild(n);
32
+ const a = ctx.animate(n, frames, { fill: 'forwards', ...opts });
33
+ const rm = () => n.remove();
34
+ if (a)
35
+ a.finished.then(rm, rm);
36
+ else
37
+ rm();
38
+ return a;
39
+ }
40
+ /** An overlay child covering `el` (makes `el` a positioning context). Returns it and a remover. */
41
+ function overlay(el, css) {
42
+ const o = document.createElement('span');
43
+ o.setAttribute('aria-hidden', 'true');
44
+ o.style.cssText = `position:absolute;inset:0;pointer-events:none;border-radius:inherit;${css}`;
45
+ if (getComputedStyle(el).position === 'static')
46
+ el.style.position = 'relative';
47
+ el.appendChild(o);
48
+ return [o, () => o.remove()];
49
+ }
50
+ const PALETTE = ['#7c5cff', '#ff5c8a', '#22d3ee', '#facc15', '#34d399'];
51
+ const rand = (a, b) => a + Math.random() * (b - a);
52
+ /** Wait for all animations (ignoring nulls). */
53
+ const all = (anims) => Promise.all(anims.filter(Boolean).map((a) => a.finished.catch(() => undefined)));
54
+
55
+ const CARD_FX = [
56
+ {
57
+ name: 'holo',
58
+ kind: 'card',
59
+ description: 'Holographic foil: a rainbow sheen and sparkle that follow the pointer (persistent; bind with trigger="load").',
60
+ reduced: 'run',
61
+ defaults: { strength: 0.55 },
62
+ run: (el, o, ctx) => {
63
+ const [layer, remove] = overlay(el, `mix-blend-mode:color-dodge;opacity:${o.strength};background:linear-gradient(115deg,transparent 20%,#ff8bd855 35%,#8bf3ff55 45%,#fff58b55 55%,transparent 70%),repeating-linear-gradient(55deg,#ffffff10 0 2px,transparent 2px 6px);background-size:250% 250%,100% 100%;background-position:50% 50%;transition:background-position .2s ease-out`);
64
+ if (ctx.reduced)
65
+ return remove;
66
+ const move = (e) => {
67
+ const r = el.getBoundingClientRect();
68
+ const x = ((e.clientX - r.left) / (r.width || 1)) * 100;
69
+ const y = ((e.clientY - r.top) / (r.height || 1)) * 100;
70
+ layer.style.backgroundPosition = `${x}% ${y}%, 0 0`;
71
+ el.style.transform = `perspective(700px) rotateY(${(x - 50) / 8}deg) rotateX(${(50 - y) / 8}deg)`;
72
+ };
73
+ const leave = () => {
74
+ layer.style.backgroundPosition = '50% 50%, 0 0';
75
+ el.style.transform = '';
76
+ };
77
+ el.addEventListener('pointermove', move);
78
+ el.addEventListener('pointerleave', leave);
79
+ return () => {
80
+ el.removeEventListener('pointermove', move);
81
+ el.removeEventListener('pointerleave', leave);
82
+ leave();
83
+ remove();
84
+ };
85
+ },
86
+ },
87
+ {
88
+ name: 'glare-sweep',
89
+ kind: 'card',
90
+ description: 'A diagonal light streak sweeps across the card once.',
91
+ defaults: { duration: 900, color: 'rgba(255,255,255,.55)' },
92
+ run: (el, o, ctx) => {
93
+ if (ctx.reduced)
94
+ return;
95
+ const [g, remove] = overlay(el, `overflow:hidden;background:linear-gradient(105deg,transparent 35%,${o.color} 50%,transparent 65%);background-size:250% 100%`);
96
+ const a = ctx.animate(g, [{ backgroundPosition: '150% 0' }, { backgroundPosition: '-50% 0' }], { duration: o.duration, easing: 'ease-in-out' });
97
+ if (a)
98
+ a.finished.then(remove, remove);
99
+ else
100
+ remove();
101
+ return a;
102
+ },
103
+ },
104
+ {
105
+ name: 'book-open',
106
+ kind: 'card',
107
+ description: 'The card opens like a book cover around its left edge, peeks and closes.',
108
+ defaults: { angle: 35, duration: 1100 },
109
+ run: (el, o, ctx) => {
110
+ el.style.transformOrigin = 'left center';
111
+ return ctx.animate(el, [{ transform: 'perspective(900px) rotateY(0)' }, { transform: `perspective(900px) rotateY(-${o.angle}deg)`, offset: 0.45 }, { transform: 'perspective(900px) rotateY(0)' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.3, 0.64, 1)' });
112
+ },
113
+ },
114
+ {
115
+ name: 'card-fan',
116
+ kind: 'card',
117
+ description: 'The element’s children fan out like a hand of cards, then gather.',
118
+ defaults: { spread: 14, duration: 900 },
119
+ run: (el, o, ctx) => {
120
+ const kids = Array.from(el.children);
121
+ const mid = (kids.length - 1) / 2;
122
+ return all(kids.map((k, i) => ctx.animate(k, [{ transform: 'none' }, { transform: `translateX(${(i - mid) * o.spread}px) rotate(${(i - mid) * (o.spread / 2)}deg)`, offset: 0.5 }, { transform: 'none' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' })));
123
+ },
124
+ },
125
+ {
126
+ name: 'topple',
127
+ kind: 'card',
128
+ description: 'Tips backwards on its bottom edge and springs upright.',
129
+ defaults: { duration: 1000 },
130
+ run: (el, o, ctx) => {
131
+ el.style.transformOrigin = 'bottom center';
132
+ return ctx.animate(el, [{ transform: 'perspective(800px) rotateX(0)' }, { transform: 'perspective(800px) rotateX(55deg)', offset: 0.35 }, { transform: 'perspective(800px) rotateX(-12deg)', offset: 0.6 }, { transform: 'perspective(800px) rotateX(5deg)', offset: 0.8 }, { transform: 'perspective(800px) rotateX(0)' }], { duration: o.duration, easing: 'ease-out' });
133
+ },
134
+ },
135
+ {
136
+ name: 'float-tilt',
137
+ kind: 'loop',
138
+ description: 'Idle floating with a slow 3D sway (loop; stops under reduced motion).',
139
+ defaults: { duration: 4200, distance: 8 },
140
+ run: (el, o, ctx) => {
141
+ const a = ctx.animate(el, [{ transform: 'translateY(0) rotateX(0) rotateY(0)' }, { transform: `translateY(-${o.distance}px) rotateX(4deg) rotateY(-5deg)` }, { transform: 'translateY(0) rotateX(0) rotateY(0)' }], { duration: o.duration, iterations: Infinity, easing: 'ease-in-out' });
142
+ return () => a?.cancel();
143
+ },
144
+ },
145
+ ];
146
+ const CLICK_FX = [
147
+ {
148
+ name: 'shockwave',
149
+ kind: 'click',
150
+ description: 'Two expanding rings from the click point.',
151
+ defaults: { color: '#7c5cff', size: 160, duration: 700 },
152
+ run: (el, o, ctx) => {
153
+ if (ctx.reduced)
154
+ return;
155
+ const { x, y } = origin(el, ctx);
156
+ return all([0, 120].map((delay) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:3px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(0)', opacity: 0.9 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' })));
157
+ },
158
+ },
159
+ {
160
+ name: 'ink-splash',
161
+ kind: 'click',
162
+ description: 'Ink blobs splatter from the click point and fade.',
163
+ defaults: { count: 9, colors: PALETTE, duration: 800 },
164
+ run: (el, o, ctx) => {
165
+ if (ctx.reduced)
166
+ return;
167
+ const { x, y } = origin(el, ctx);
168
+ return all(Array.from({ length: o.count }, (_, i) => {
169
+ const s = rand(6, 22);
170
+ const ang = rand(0, Math.PI * 2);
171
+ const d = rand(20, 70);
172
+ return spawn(x, y, `width:${s}px;height:${s}px;margin:${-s / 2}px 0 0 ${-s / 2}px;background:${o.colors[i % o.colors.length]};border-radius:${rand(40, 50)}% ${rand(50, 60)}% ${rand(40, 60)}% ${rand(45, 55)}%`, ctx, [{ transform: 'translate(0,0) scale(.3)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) scale(1)`, opacity: 0.9, offset: 0.5 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d + 8}px) scale(1.1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' });
173
+ }));
174
+ },
175
+ },
176
+ {
177
+ name: 'star-burst',
178
+ kind: 'click',
179
+ description: 'Spinning stars burst outward.',
180
+ defaults: { count: 10, colors: ['#facc15', '#fb923c', '#f472b6'], duration: 900 },
181
+ run: (el, o, ctx) => {
182
+ if (ctx.reduced)
183
+ return;
184
+ const { x, y } = origin(el, ctx);
185
+ return all(Array.from({ length: o.count }, (_, i) => {
186
+ const ang = (i / o.count) * Math.PI * 2;
187
+ const d = rand(40, 80);
188
+ const s = rand(12, 22);
189
+ return spawn(x, y, `font-size:${s}px;line-height:1;margin:${-s / 2}px 0 0 ${-s / 2}px;color:${o.colors[i % o.colors.length]}`, ctx, [{ transform: 'translate(0,0) rotate(0) scale(.2)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) rotate(${rand(180, 360)}deg) scale(1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' }, '★');
190
+ }));
191
+ },
192
+ },
193
+ {
194
+ name: 'jelly-press',
195
+ kind: 'click',
196
+ description: 'The pressed element squishes like jelly and wobbles back.',
197
+ defaults: { duration: 650 },
198
+ run: (el, o, ctx) => ctx.reduced ? ctx.animate(el, [{ opacity: 1 }, { opacity: 0.7 }, { opacity: 1 }], { duration: 250 }) : ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: 'scale(1.15,.85)', offset: 0.2 }, { transform: 'scale(.9,1.1)', offset: 0.4 }, { transform: 'scale(1.05,.95)', offset: 0.6 }, { transform: 'scale(.98,1.02)', offset: 0.8 }, { transform: 'scale(1,1)' }], { duration: o.duration, easing: 'ease-out' }),
199
+ },
200
+ {
201
+ name: 'ring-ripple',
202
+ kind: 'click',
203
+ description: 'Concentric outlined rings ripple out like water.',
204
+ defaults: { rings: 3, color: '#22d3ee', size: 120, duration: 1000 },
205
+ run: (el, o, ctx) => {
206
+ if (ctx.reduced)
207
+ return;
208
+ const { x, y } = origin(el, ctx);
209
+ return all(Array.from({ length: o.rings }, (_, i) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:2px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(.1)', opacity: 0.8 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay: i * 160, easing: 'ease-out' })));
210
+ },
211
+ },
212
+ {
213
+ name: 'emoji-rain',
214
+ kind: 'click',
215
+ description: 'Emoji pop up and fall around the click (pass `emoji: "🎉✨💜"`).',
216
+ defaults: { emoji: '🎉✨💜🔥', count: 14, duration: 1300 },
217
+ run: (el, o, ctx) => {
218
+ if (ctx.reduced)
219
+ return;
220
+ const { x, y } = origin(el, ctx);
221
+ const chars = Array.from(String(o.emoji));
222
+ return all(Array.from({ length: o.count }, (_, i) => {
223
+ const dx = rand(-90, 90);
224
+ return spawn(x, y, `font-size:${rand(16, 28)}px;line-height:1`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 0 }, { transform: `translate(${dx * 0.6}px,${rand(-90, -50)}px) scale(1)`, opacity: 1, offset: 0.35 }, { transform: `translate(${dx}px,${rand(40, 90)}px) rotate(${rand(-60, 60)}deg) scale(.9)`, opacity: 0 }], { duration: o.duration, delay: i * 25, easing: 'cubic-bezier(0.33, 0, 0.67, 1)' }, chars[i % chars.length]);
225
+ }));
226
+ },
227
+ },
228
+ ];
229
+
230
+ /**
231
+ * Sample a damped spring from 0 → 1 and return the progress values plus the
232
+ * time (ms) it takes to settle. Pure, deterministic.
233
+ */
234
+ function solveSpring({ stiffness = 180, damping = 12, mass = 1, steps = 40 } = {}) {
235
+ const dt = 1 / 120;
236
+ let x = 0;
237
+ let v = 0;
238
+ const trace = [];
239
+ let t = 0;
240
+ for (; t < 4; t += dt) {
241
+ const a = (-stiffness * (x - 1) - damping * v) / mass;
242
+ v += a * dt;
243
+ x += v * dt;
244
+ trace.push(x);
245
+ if (t > 0.1 && Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.01)
246
+ break;
247
+ }
248
+ const values = Array.from({ length: steps + 1 }, (_, i) => +trace[Math.min(trace.length - 1, Math.round((i / steps) * (trace.length - 1)))].toFixed(4));
249
+ values[0] = 0;
250
+ values[steps] = 1;
251
+ return { values, duration: Math.round(trace.length * dt * 1000) };
252
+ }
253
+ /** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
254
+ function springKeyframes(map, spring) {
255
+ const { values, duration } = solveSpring(spring);
256
+ return { frames: values.map(map), duration };
257
+ }
258
+ /** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
259
+ function bounceKeyframes(bounce = 0.5, steps = 48) {
260
+ // segment durations scale with sqrt(height); heights scale with bounce^2 per hop
261
+ const hops = [1];
262
+ let h = 1;
263
+ while (hops.length < 6 && h > 0.01) {
264
+ h *= bounce * bounce;
265
+ hops.push(h);
266
+ }
267
+ const segT = hops.map((hh, i) => (i === 0 ? Math.sqrt(hh) : 2 * Math.sqrt(hh)));
268
+ const total = segT.reduce((a, b) => a + b, 0);
269
+ return Array.from({ length: steps + 1 }, (_, i) => {
270
+ let t = (i / steps) * total;
271
+ for (let k = 0; k < hops.length; k++) {
272
+ if (t <= segT[k] || k === hops.length - 1) {
273
+ const tt = Math.min(t, segT[k]);
274
+ if (k === 0)
275
+ return +(1 - (tt / segT[0]) ** 2).toFixed(4);
276
+ const half = segT[k] / 2;
277
+ return +(hops[k] * (1 - ((tt - half) / half) ** 2)).toFixed(4);
278
+ }
279
+ t -= segT[k];
280
+ }
281
+ return 0;
282
+ });
283
+ }
284
+ const fade = (el, ctx) => ctx.animate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
285
+ const PHYSICS_FX = [
286
+ {
287
+ name: 'bounce-in',
288
+ kind: 'enter',
289
+ description: 'Springs in from small with an overshoot (spring solver; stiffness / damping options).',
290
+ defaults: { stiffness: 220, damping: 11, from: 0.3 },
291
+ run: (el, o, ctx) => {
292
+ if (ctx.reduced)
293
+ return fade(el, ctx);
294
+ const { frames, duration } = springKeyframes((p) => ({ transform: `scale(${(o.from + (1 - o.from) * p).toFixed(4)})`, opacity: Math.min(1, p * 3) }), o);
295
+ return ctx.animate(el, frames, { duration, easing: 'linear' });
296
+ },
297
+ },
298
+ {
299
+ name: 'rubber-band',
300
+ kind: 'attention',
301
+ description: 'Stretches wide, snaps back thin and settles like a rubber band.',
302
+ defaults: { duration: 900, amount: 0.25 },
303
+ run: (el, o, ctx) => {
304
+ if (ctx.reduced)
305
+ return;
306
+ const a = o.amount;
307
+ return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${1 + a},${1 - a})`, offset: 0.3 }, { transform: `scale(${1 - a},${1 + a})`, offset: 0.4 }, { transform: `scale(${1 + a / 2},${1 - a / 2})`, offset: 0.5 }, { transform: `scale(${1 - a / 5},${1 + a / 5})`, offset: 0.65 }, { transform: `scale(${1 + a / 10},${1 - a / 10})`, offset: 0.75 }, { transform: 'scale(1,1)' }], { duration: o.duration });
308
+ },
309
+ },
310
+ {
311
+ name: 'elastic-hover',
312
+ kind: 'hover',
313
+ description: 'Hover lifts with a springy overshoot; leaving springs back (persistent; trigger="load").',
314
+ defaults: { lift: 6, scale: 1.04, stiffness: 260, damping: 10 },
315
+ run: (el, o, ctx) => {
316
+ if (ctx.reduced)
317
+ return;
318
+ const to = (on) => {
319
+ const from = on ? 0 : 1;
320
+ const { frames, duration } = springKeyframes((p) => {
321
+ const k = from + (on ? p : -p);
322
+ return { transform: `translateY(${(-o.lift * k).toFixed(2)}px) scale(${(1 + (o.scale - 1) * k).toFixed(4)})` };
323
+ }, o);
324
+ ctx.animate(el, frames, { duration, fill: 'forwards', easing: 'linear' });
325
+ };
326
+ const enter = () => to(true);
327
+ const leave = () => to(false);
328
+ el.addEventListener('pointerenter', enter);
329
+ el.addEventListener('pointerleave', leave);
330
+ return () => {
331
+ el.removeEventListener('pointerenter', enter);
332
+ el.removeEventListener('pointerleave', leave);
333
+ };
334
+ },
335
+ },
336
+ {
337
+ name: 'drop-bounce',
338
+ kind: 'enter',
339
+ description: 'Falls in under gravity and bounces to rest (`height`, `bounce` restitution 0–0.9).',
340
+ defaults: { height: 120, bounce: 0.5, duration: 1100 },
341
+ run: (el, o, ctx) => {
342
+ if (ctx.reduced)
343
+ return fade(el, ctx);
344
+ const hs = bounceKeyframes(Math.min(0.9, Math.max(0, o.bounce)));
345
+ return ctx.animate(el, hs.map((h, i) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)`, opacity: i === 0 ? 0 : 1 })), { duration: o.duration, easing: 'linear' });
346
+ },
347
+ },
348
+ {
349
+ name: 'gravity-text',
350
+ kind: 'text',
351
+ description: 'Each character drops in under gravity with a bounce, staggered (splits the text into aria-hidden spans; the label stays readable).',
352
+ defaults: { height: 60, bounce: 0.45, duration: 900, stagger: 45 },
353
+ run: (el, o, ctx) => {
354
+ if (ctx.reduced)
355
+ return;
356
+ if (!el.dataset.usaSplit) {
357
+ const text = el.textContent || '';
358
+ el.setAttribute('aria-label', text);
359
+ el.textContent = '';
360
+ for (const ch of Array.from(text)) {
361
+ const s = document.createElement('span');
362
+ s.setAttribute('aria-hidden', 'true');
363
+ s.style.display = 'inline-block';
364
+ s.style.whiteSpace = 'pre';
365
+ s.textContent = ch;
366
+ el.appendChild(s);
367
+ }
368
+ el.dataset.usaSplit = '1';
369
+ }
370
+ const hs = bounceKeyframes(o.bounce, 32);
371
+ return all(Array.from(el.children).map((s, i) => ctx.animate(s, hs.map((h) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)` })), { duration: o.duration, delay: i * o.stagger, easing: 'linear', fill: 'backwards' })));
372
+ },
373
+ },
374
+ {
375
+ name: 'spring-follow',
376
+ kind: 'cursor',
377
+ description: 'The element springs toward the pointer while it moves over its parent, and back home on leave (persistent).',
378
+ defaults: { stiffness: 0.12, damping: 0.75, range: 40 },
379
+ run: (el, o, ctx) => {
380
+ if (ctx.reduced)
381
+ return;
382
+ const host = el.parentElement || el;
383
+ let tx = 0;
384
+ let ty = 0;
385
+ let x = 0;
386
+ let y = 0;
387
+ let vx = 0;
388
+ let vy = 0;
389
+ let raf = 0;
390
+ const tick = () => {
391
+ vx = (vx + (tx - x) * o.stiffness) * o.damping;
392
+ vy = (vy + (ty - y) * o.stiffness) * o.damping;
393
+ x += vx;
394
+ y += vy;
395
+ el.style.translate = `${x.toFixed(2)}px ${y.toFixed(2)}px`;
396
+ raf = Math.abs(vx) + Math.abs(vy) + Math.abs(tx - x) + Math.abs(ty - y) > 0.05 ? requestAnimationFrame(tick) : 0;
397
+ };
398
+ const kick = () => {
399
+ if (!raf)
400
+ raf = requestAnimationFrame(tick);
401
+ };
402
+ const move = (e) => {
403
+ const r = host.getBoundingClientRect();
404
+ const nx = (e.clientX - r.left) / (r.width || 1) - 0.5;
405
+ const ny = (e.clientY - r.top) / (r.height || 1) - 0.5;
406
+ tx = nx * 2 * o.range;
407
+ ty = ny * 2 * o.range;
408
+ kick();
409
+ };
410
+ const leave = () => {
411
+ tx = ty = 0;
412
+ kick();
413
+ };
414
+ host.addEventListener('pointermove', move);
415
+ host.addEventListener('pointerleave', leave);
416
+ return () => {
417
+ host.removeEventListener('pointermove', move);
418
+ host.removeEventListener('pointerleave', leave);
419
+ cancelAnimationFrame(raf);
420
+ el.style.translate = '';
421
+ };
422
+ },
423
+ },
424
+ {
425
+ name: 'bell-swing',
426
+ kind: 'attention',
427
+ description: 'Swings from its top like a ringing bell with damped oscillation.',
428
+ defaults: { angle: 22, stiffness: 120, damping: 4 },
429
+ run: (el, o, ctx) => {
430
+ if (ctx.reduced)
431
+ return;
432
+ el.style.transformOrigin = 'top center';
433
+ const { frames, duration } = springKeyframes((p) => ({ transform: `rotate(${(o.angle * (1 - p)).toFixed(2)}deg)` }), { stiffness: o.stiffness, damping: o.damping, steps: 48 });
434
+ frames[0] = { transform: 'rotate(0deg)' };
435
+ return ctx.animate(el, frames, { duration: Math.min(duration, 2400), easing: 'linear' });
436
+ },
437
+ },
438
+ ];
439
+
440
+ /**
441
+ * use-scroll-animate/components/effects — the 5.x effect packs, all
442
+ * registered through `registerEffect()` (5.0) and playable with
443
+ * `playEffect()`, `bindEffect()` or `<usa-fx>`. Kept out of
444
+ * `use-scroll-animate/components` / `components/lite` so their size budgets
445
+ * hold; the UMD bundle registers everything.
446
+ *
447
+ * ```ts
448
+ * import { registerAllEffects } from 'use-scroll-animate/components/effects';
449
+ * registerAllEffects();
450
+ * ```
451
+ */
452
+ /** The effect packs by version, in release order. */
453
+ const EFFECT_PACKS = {
454
+ 'cards-click': [...CARD_FX, ...CLICK_FX],
455
+ physics: PHYSICS_FX,
456
+ };
457
+ /** 5.1: card & click effects 2.0. */
458
+ function registerCardClickEffects() {
459
+ registerEffects(EFFECT_PACKS['cards-click']);
460
+ }
461
+ /** 5.2: bounce & physics micro-interactions. */
462
+ function registerPhysicsEffects() {
463
+ registerEffects(EFFECT_PACKS.physics);
464
+ }
465
+ /** Register the built-ins and every pack (idempotent). */
466
+ function registerAllEffects() {
467
+ registerBuiltinEffects();
468
+ for (const defs of Object.values(EFFECT_PACKS))
469
+ registerEffects(defs);
470
+ }
471
+
472
+ export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
473
+ //# sourceMappingURL=effects.js.map