use-scroll-animate 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.
package/dist/index.esm.js CHANGED
@@ -1,697 +1,1179 @@
1
- /**
2
- * use-scroll-animate - Animation Presets
3
- * Defines keyframes for all built-in animation presets
4
- */
5
- const PRESETS = {
6
- 'fade-in': {
7
- from: { opacity: 0 },
8
- to: { opacity: 1 },
9
- },
10
- 'fade-in-up': {
11
- from: { opacity: 0, transform: 'translateY(40px)' },
12
- to: { opacity: 1, transform: 'translateY(0)' },
13
- },
14
- 'fade-in-down': {
15
- from: { opacity: 0, transform: 'translateY(-40px)' },
16
- to: { opacity: 1, transform: 'translateY(0)' },
17
- },
18
- 'fade-in-left': {
19
- from: { opacity: 0, transform: 'translateX(-40px)' },
20
- to: { opacity: 1, transform: 'translateX(0)' },
21
- },
22
- 'fade-in-right': {
23
- from: { opacity: 0, transform: 'translateX(40px)' },
24
- to: { opacity: 1, transform: 'translateX(0)' },
25
- },
26
- 'zoom-in': {
27
- from: { opacity: 0, transform: 'scale(0.8)' },
28
- to: { opacity: 1, transform: 'scale(1)' },
29
- },
30
- 'zoom-out': {
31
- from: { opacity: 0, transform: 'scale(1.2)' },
32
- to: { opacity: 1, transform: 'scale(1)' },
33
- },
34
- 'flip-x': {
35
- from: { opacity: 0, transform: 'rotateX(-90deg)' },
36
- to: { opacity: 1, transform: 'rotateX(0)' },
37
- },
38
- 'flip-y': {
39
- from: { opacity: 0, transform: 'rotateY(-90deg)' },
40
- to: { opacity: 1, transform: 'rotateY(0)' },
41
- },
42
- 'slide-up': {
43
- from: { transform: 'translateY(100%)' },
44
- to: { transform: 'translateY(0)' },
45
- },
46
- 'slide-down': {
47
- from: { transform: 'translateY(-100%)' },
48
- to: { transform: 'translateY(0)' },
49
- },
50
- 'slide-left': {
51
- from: { transform: 'translateX(-100%)' },
52
- to: { transform: 'translateX(0)' },
53
- },
54
- 'slide-right': {
55
- from: { transform: 'translateX(100%)' },
56
- to: { transform: 'translateX(0)' },
57
- },
58
- 'bounce': {
59
- from: { opacity: 0, transform: 'translateY(-60px)' },
60
- to: { opacity: 1, transform: 'translateY(0)' },
61
- },
62
- 'rotate-in': {
63
- from: { opacity: 0, transform: 'rotate(-180deg) scale(0.5)' },
64
- to: { opacity: 1, transform: 'rotate(0deg) scale(1)' },
65
- },
66
- 'blur-in': {
67
- from: { opacity: 0, filter: 'blur(12px)' },
68
- to: { opacity: 1, filter: 'blur(0px)' },
69
- },
70
- 'skew-in': {
71
- from: { opacity: 0, transform: 'skewX(20deg) translateX(30px)' },
72
- to: { opacity: 1, transform: 'skewX(0deg) translateX(0)' },
73
- },
74
- 'scale-x': {
75
- from: { transform: 'scaleX(0)' },
76
- to: { transform: 'scaleX(1)' },
77
- },
78
- 'scale-y': {
79
- from: { transform: 'scaleY(0)' },
80
- to: { transform: 'scaleY(1)' },
81
- },
82
- 'shimmer': {
83
- from: { opacity: 0.5, filter: 'brightness(1)' },
84
- to: { opacity: 1, filter: 'brightness(1.5)' },
85
- },
86
- 'pulse': {
87
- from: { transform: 'scale(1)' },
88
- to: { transform: 'scale(1.05)' },
89
- },
90
- 'swing': {
91
- from: { transform: 'rotate(-10deg)' },
92
- to: { transform: 'rotate(10deg)' },
93
- },
94
- };
95
- function resolvePreset(animation) {
96
- var _a;
97
- if (typeof animation === 'string') {
98
- return (_a = PRESETS[animation]) !== null && _a !== void 0 ? _a : PRESETS['fade-in-up'];
99
- }
100
- if (Array.isArray(animation)) {
101
- const combined = { from: {}, to: {} };
102
- animation.forEach(name => {
103
- const preset = PRESETS[name];
104
- if (preset) {
105
- Object.entries(preset.from).forEach(([key, val]) => {
106
- if (key === 'transform' && combined.from[key]) {
107
- combined.from[key] = `${combined.from[key]} ${val}`;
108
- }
109
- else {
110
- combined.from[key] = val;
111
- }
112
- });
113
- Object.entries(preset.to).forEach(([key, val]) => {
114
- if (key === 'transform' && combined.to[key]) {
115
- combined.to[key] = `${combined.to[key]} ${val}`;
116
- }
117
- else {
118
- combined.to[key] = val;
119
- }
120
- });
121
- }
122
- });
123
- return combined;
124
- }
125
- return animation;
126
- }
127
- /** Easing to CSS cubic-bezier mapping */
128
- const EASING_MAP = {
129
- linear: 'linear',
130
- ease: 'ease',
131
- 'ease-in': 'ease-in',
132
- 'ease-out': 'ease-out',
133
- 'ease-in-out': 'ease-in-out',
134
- spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
135
- };
136
- function resolveEasing(easing) {
137
- var _a;
138
- return (_a = EASING_MAP[easing]) !== null && _a !== void 0 ? _a : easing;
139
- }
140
-
141
- /**
142
- * use-scroll-animate - Core Implementation
143
- * Uses IntersectionObserver + Web Animations API for zero-dependency,
144
- * high-performance scroll-triggered animations.
145
- */
146
- const DEFAULT_CONFIG = {
147
- defaultAnimation: 'fade-in-up',
148
- defaultDuration: 600,
149
- defaultDelay: 0,
150
- defaultEasing: 'ease',
151
- defaultThreshold: 0.1,
152
- defaultRootMargin: '0px',
153
- defaultRepeat: false,
154
- defaultOnce: true,
155
- defaultOffset: 0,
156
- hiddenClass: 'sa-hidden',
157
- visibleClass: 'sa-visible',
158
- useClassNames: false,
159
- disabled: false,
160
- root: null,
161
- };
162
- const DEFAULT_OPTIONS = {
163
- stagger: 0,
164
- parallax: {},
165
- onStart: () => undefined,
166
- onComplete: () => undefined,
167
- onEnter: () => undefined,
168
- onLeave: () => undefined,
169
- onProgress: () => undefined,
170
- };
171
- function parseDataAttributes(el, config) {
172
- const dataset = el.dataset;
173
- const opts = {};
174
- if (dataset.saAnimation) {
175
- const anim = dataset.saAnimation;
176
- opts.animation = anim.includes(',')
177
- ? anim.split(',').map(s => s.trim())
178
- : anim;
179
- }
180
- if (dataset.saDuration)
181
- opts.duration = parseInt(dataset.saDuration, 10);
182
- if (dataset.saDelay)
183
- opts.delay = parseInt(dataset.saDelay, 10);
184
- if (dataset.saEasing)
185
- opts.easing = dataset.saEasing;
186
- if (dataset.saThreshold) {
187
- const t = dataset.saThreshold;
188
- opts.threshold = t.includes(',') ? t.split(',').map(parseFloat) : parseFloat(t);
189
- }
190
- if (dataset.saRootMargin)
191
- opts.rootMargin = dataset.saRootMargin;
192
- if (dataset.saRepeat !== undefined)
193
- opts.repeat = dataset.saRepeat !== 'false';
194
- if (dataset.saOnce !== undefined)
195
- opts.once = dataset.saOnce !== 'false';
196
- if (dataset.saOffset)
197
- opts.offset = parseInt(dataset.saOffset, 10);
198
- if (dataset.saStagger)
199
- opts.stagger = parseInt(dataset.saStagger, 10);
200
- if (dataset.saParallaxX || dataset.saParallaxY || dataset.saParallaxRotate || dataset.saParallaxScale) {
201
- opts.parallax = {
202
- x: dataset.saParallaxX,
203
- y: dataset.saParallaxY,
204
- rotate: dataset.saParallaxRotate ? parseFloat(dataset.saParallaxRotate) : undefined,
205
- scale: dataset.saParallaxScale ? parseFloat(dataset.saParallaxScale) : undefined,
206
- speed: dataset.saParallaxSpeed ? parseFloat(dataset.saParallaxSpeed) : 1,
207
- };
208
- }
209
- return mergeOptions(opts, config);
210
- }
211
- function mergeOptions(opts, config) {
212
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
213
- const repeat = (_a = opts.repeat) !== null && _a !== void 0 ? _a : config.defaultRepeat;
214
- return {
215
- animation: (_b = opts.animation) !== null && _b !== void 0 ? _b : config.defaultAnimation,
216
- duration: (_c = opts.duration) !== null && _c !== void 0 ? _c : config.defaultDuration,
217
- delay: (_d = opts.delay) !== null && _d !== void 0 ? _d : config.defaultDelay,
218
- easing: (_e = opts.easing) !== null && _e !== void 0 ? _e : config.defaultEasing,
219
- threshold: (_f = opts.threshold) !== null && _f !== void 0 ? _f : config.defaultThreshold,
220
- rootMargin: (_g = opts.rootMargin) !== null && _g !== void 0 ? _g : config.defaultRootMargin,
221
- repeat: repeat,
222
- once: (_h = opts.once) !== null && _h !== void 0 ? _h : (repeat ? false : config.defaultOnce),
223
- offset: (_j = opts.offset) !== null && _j !== void 0 ? _j : config.defaultOffset,
224
- stagger: (_k = opts.stagger) !== null && _k !== void 0 ? _k : DEFAULT_OPTIONS.stagger,
225
- parallax: (_l = opts.parallax) !== null && _l !== void 0 ? _l : DEFAULT_OPTIONS.parallax,
226
- onStart: (_m = opts.onStart) !== null && _m !== void 0 ? _m : DEFAULT_OPTIONS.onStart,
227
- onComplete: (_o = opts.onComplete) !== null && _o !== void 0 ? _o : DEFAULT_OPTIONS.onComplete,
228
- onEnter: (_p = opts.onEnter) !== null && _p !== void 0 ? _p : DEFAULT_OPTIONS.onEnter,
229
- onLeave: (_q = opts.onLeave) !== null && _q !== void 0 ? _q : DEFAULT_OPTIONS.onLeave,
230
- onProgress: (_r = opts.onProgress) !== null && _r !== void 0 ? _r : DEFAULT_OPTIONS.onProgress,
231
- };
232
- }
233
- function resolveTargets(target) {
234
- if (typeof target === 'string') {
235
- return Array.from(document.querySelectorAll(target));
236
- }
237
- if (target instanceof Element) {
238
- return [target];
239
- }
240
- if (target instanceof NodeList) {
241
- return Array.from(target);
242
- }
243
- if (Array.isArray(target)) {
244
- return target;
245
- }
246
- return [];
247
- }
248
- function prefersReducedMotion() {
249
- var _a, _b;
250
- return (_b = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, '(prefers-reduced-motion: reduce)').matches) !== null && _b !== void 0 ? _b : false;
251
- }
252
- function runAnimation(el, opts, config, staggerIndex = 0) {
253
- const { animation, duration, delay, easing, stagger, onStart, onComplete } = opts;
254
- const totalDelay = delay + staggerIndex * stagger;
255
- const preset = resolvePreset(animation);
256
- const easingValue = resolveEasing(easing);
257
- if (config.useClassNames) {
258
- if (totalDelay > 0) {
259
- el.style.animationDelay = `${totalDelay}ms`;
260
- }
261
- el.classList.remove(config.hiddenClass);
262
- el.classList.add(config.visibleClass);
263
- onStart(el);
264
- setTimeout(() => onComplete(el), duration + totalDelay);
265
- return;
266
- }
267
- const keyframes = [
268
- preset.from,
269
- preset.to,
270
- ];
271
- const timing = {
272
- duration,
273
- delay: totalDelay,
274
- easing: easingValue,
275
- fill: 'both',
276
- };
277
- // Check if Web Animations API is supported
278
- if (typeof el.animate === 'function') {
279
- const anim = el.animate(keyframes, timing);
280
- onStart(el);
281
- anim.onfinish = () => onComplete(el);
282
- }
283
- else {
284
- // Fallback for older browsers
285
- el.style.opacity = '1';
286
- el.style.transform = 'none';
287
- onStart(el);
288
- onComplete(el);
289
- }
290
- }
291
- function applyParallax(el, progress, parallax) {
292
- const { x = 0, y = 0, rotate = 0, scale = 1, speed = 1 } = parallax;
293
- const p = (progress - 0.5) * 2 * speed;
294
- let transform = '';
295
- if (x)
296
- transform += ` translateX(${typeof x === 'number' ? x * p + 'px' : 'calc(' + x + ' * ' + p + ')'})`;
297
- if (y)
298
- transform += ` translateY(${typeof y === 'number' ? y * p + 'px' : 'calc(' + y + ' * ' + p + ')'})`;
299
- if (rotate)
300
- transform += ` rotate(${rotate * p}deg)`;
301
- if (scale !== 1)
302
- transform += ` scale(${1 + (scale - 1) * p})`;
303
- el.style.transform = transform;
304
- }
305
- function hideElement(el, config) {
306
- if (config.useClassNames) {
307
- el.classList.add(config.hiddenClass);
308
- el.classList.remove(config.visibleClass);
309
- }
310
- else {
311
- el.style.opacity = '0';
312
- }
313
- }
314
- function createScrollAnimate(userConfig = {}) {
315
- let config = { ...DEFAULT_CONFIG, ...userConfig };
316
- const registry = new Map();
317
- function createObserver(opts) {
318
- // Handle offset by adjusting rootMargin
319
- const rootMargin = opts.offset !== 0
320
- ? `${opts.rootMargin.split(' ')[0]} 0px -${opts.offset}px 0px`
321
- : opts.rootMargin;
322
- return new IntersectionObserver((entries) => {
323
- entries.forEach((entry) => {
324
- var _a;
325
- const record = registry.get(entry.target);
326
- if (!record)
327
- return;
328
- if (entry.isIntersecting) {
329
- opts.onEnter(entry.target);
330
- if (!record.animated || opts.repeat) {
331
- const parent = entry.target.parentElement;
332
- let staggerIndex = 0;
333
- if (parent && opts.stagger > 0) {
334
- // BUG FIX: Use a more robust way to find index among siblings that are also observed
335
- const siblings = Array.from(parent.children).filter((c) => registry.has(c));
336
- staggerIndex = siblings.indexOf(entry.target);
337
- }
338
- if (!config.disabled && !prefersReducedMotion()) {
339
- runAnimation(entry.target, opts, config, staggerIndex);
340
- }
341
- else {
342
- entry.target.style.opacity = '1';
343
- entry.target.style.transform = '';
344
- }
345
- record.animated = true;
346
- // Handle 'once' functionality
347
- if (opts.once) {
348
- record.observer.unobserve(entry.target);
349
- (_a = record.progressObserver) === null || _a === void 0 ? void 0 : _a.unobserve(entry.target);
350
- // We don't delete from registry yet to allow other potential interactions
351
- }
352
- }
353
- }
354
- else {
355
- opts.onLeave(entry.target);
356
- if (opts.repeat && record.animated) {
357
- hideElement(entry.target, config);
358
- record.animated = false;
359
- }
360
- }
361
- });
362
- }, {
363
- threshold: typeof opts.threshold === 'number' ? opts.threshold : opts.threshold[0],
364
- rootMargin: rootMargin,
365
- root: config.root,
366
- });
367
- }
368
- function createProgressObserver(el, opts) {
369
- const thresholds = [];
370
- for (let i = 0; i <= 100; i++)
371
- thresholds.push(i / 100);
372
- return new IntersectionObserver((entries) => {
373
- entries.forEach((entry) => {
374
- const progress = entry.intersectionRatio;
375
- opts.onProgress(el, progress);
376
- if (Object.keys(opts.parallax).length > 0) {
377
- applyParallax(el, progress, opts.parallax);
378
- }
379
- });
380
- }, {
381
- threshold: thresholds,
382
- rootMargin: opts.rootMargin,
383
- root: config.root,
384
- });
385
- }
386
- function observeElement(el, opts) {
387
- if (registry.has(el))
388
- return;
389
- if (!config.useClassNames) {
390
- hideElement(el, config);
391
- }
392
- const observer = createObserver(opts);
393
- observer.observe(el);
394
- let progressObserver;
395
- if (Object.keys(opts.parallax).length > 0 || opts.onProgress !== DEFAULT_OPTIONS.onProgress) {
396
- progressObserver = createProgressObserver(el, opts);
397
- progressObserver.observe(el);
398
- }
399
- registry.set(el, {
400
- element: el,
401
- options: opts,
402
- observer,
403
- animated: false,
404
- progressObserver
405
- });
406
- }
407
- const instance = {
408
- observe(target, options = {}) {
409
- const elements = resolveTargets(target);
410
- const opts = mergeOptions(options, config);
411
- elements.forEach((el) => observeElement(el, opts));
412
- },
413
- unobserve(target) {
414
- const elements = resolveTargets(target);
415
- elements.forEach((el) => {
416
- var _a;
417
- const record = registry.get(el);
418
- if (record) {
419
- record.observer.disconnect(); // BUG FIX: Use disconnect for better cleanup
420
- (_a = record.progressObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
421
- registry.delete(el);
422
- }
423
- });
424
- },
425
- init(rootElement = document) {
426
- const elements = rootElement.querySelectorAll('[data-sa]');
427
- elements.forEach((el) => {
428
- const opts = parseDataAttributes(el, config);
429
- observeElement(el, opts);
430
- });
431
- },
432
- destroy() {
433
- registry.forEach((record) => {
434
- var _a;
435
- record.observer.disconnect();
436
- (_a = record.progressObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
437
- });
438
- registry.clear();
439
- },
440
- refresh() {
441
- const entries = Array.from(registry.entries());
442
- instance.destroy();
443
- entries.forEach(([el, record]) => {
444
- observeElement(el, record.options);
445
- });
446
- },
447
- animate(target, options = {}) {
448
- const elements = resolveTargets(target);
449
- const opts = mergeOptions(options, config);
450
- elements.forEach((el) => {
451
- runAnimation(el, opts, config, 0);
452
- });
453
- },
454
- getObservedElements() {
455
- return Array.from(registry.values());
456
- },
457
- configure(newConfig) {
458
- config = { ...config, ...newConfig };
459
- },
460
- };
461
- return instance;
462
- }
463
-
464
- /**
465
- * use-scroll-animate - React Integration
466
- * Provides useScrollAnimate and useScrollRef hooks for React applications.
467
- */
468
- function createReactHooks(React) {
469
- function useScrollAnimate(options = {}) {
470
- const ref = React.useRef(null);
471
- React.useEffect(() => {
472
- const el = ref.current;
473
- if (!el)
474
- return;
475
- const { animation = 'fade-in-up', duration = 600, delay = 0, easing = 'ease', threshold = 0.1, rootMargin = '0px', repeat = false, parallax = {}, onStart, onComplete, onEnter, onLeave, onProgress, } = options;
476
- const preset = resolvePreset(animation);
477
- const easingValue = resolveEasing(easing);
478
- let animated = false;
479
- el.style.opacity = '0';
480
- const observer = new IntersectionObserver((entries) => {
481
- entries.forEach((entry) => {
482
- if (entry.isIntersecting) {
483
- onEnter === null || onEnter === void 0 ? void 0 : onEnter(el);
484
- if (!animated || repeat) {
485
- const keyframes = [
486
- preset.from,
487
- preset.to,
488
- ];
489
- const anim = el.animate(keyframes, {
490
- duration,
491
- delay,
492
- easing: easingValue,
493
- fill: 'both',
494
- });
495
- onStart === null || onStart === void 0 ? void 0 : onStart(el);
496
- anim.onfinish = () => onComplete === null || onComplete === void 0 ? void 0 : onComplete(el);
497
- animated = true;
498
- if (!repeat && !Object.keys(parallax).length && !onProgress) {
499
- observer.unobserve(el);
500
- }
501
- }
502
- }
503
- else {
504
- onLeave === null || onLeave === void 0 ? void 0 : onLeave(el);
505
- if (repeat && animated) {
506
- el.style.opacity = '0';
507
- animated = false;
508
- }
509
- }
510
- });
511
- }, { threshold: typeof threshold === 'number' ? threshold : threshold[0], rootMargin });
512
- let progressObserver = null;
513
- if (Object.keys(parallax).length > 0 || onProgress) {
514
- const thresholds = [];
515
- for (let i = 0; i <= 100; i++)
516
- thresholds.push(i / 100);
517
- progressObserver = new IntersectionObserver((entries) => {
518
- entries.forEach((entry) => {
519
- const progress = entry.intersectionRatio;
520
- onProgress === null || onProgress === void 0 ? void 0 : onProgress(el, progress);
521
- if (Object.keys(parallax).length > 0) {
522
- const { x = 0, y = 0, rotate = 0, scale = 1, speed = 1 } = parallax;
523
- const p = (progress - 0.5) * 2 * speed;
524
- let transform = '';
525
- if (x)
526
- transform += ` translateX(${typeof x === 'number' ? x * p + 'px' : 'calc(' + x + ' * ' + p + ')'})`;
527
- if (y)
528
- transform += ` translateY(${typeof y === 'number' ? y * p + 'px' : 'calc(' + y + ' * ' + p + ')'})`;
529
- if (rotate)
530
- transform += ` rotate(${rotate * p}deg)`;
531
- if (scale !== 1)
532
- transform += ` scale(${1 + (scale - 1) * p})`;
533
- el.style.transform = transform;
534
- }
535
- });
536
- }, { threshold: thresholds, rootMargin });
537
- progressObserver.observe(el);
538
- }
539
- observer.observe(el);
540
- return () => {
541
- observer.disconnect();
542
- progressObserver === null || progressObserver === void 0 ? void 0 : progressObserver.disconnect();
543
- };
544
- }, []);
545
- return ref;
546
- }
547
- function useScrollStagger(options = {}) {
548
- const ref = React.useRef(null);
549
- React.useEffect(() => {
550
- const container = ref.current;
551
- if (!container)
552
- return;
553
- const { animation = 'fade-in-up', duration = 600, delay = 0, easing = 'ease', threshold = 0.1, rootMargin = '0px', stagger = 80, } = options;
554
- const children = Array.from(container.children);
555
- const preset = resolvePreset(animation);
556
- const easingValue = resolveEasing(easing);
557
- children.forEach((child) => {
558
- child.style.opacity = '0';
559
- });
560
- let animated = false;
561
- const observer = new IntersectionObserver((entries) => {
562
- entries.forEach((entry) => {
563
- if (entry.isIntersecting && !animated) {
564
- animated = true;
565
- children.forEach((child, i) => {
566
- child.animate([preset.from, preset.to], {
567
- duration,
568
- delay: delay + i * stagger,
569
- easing: easingValue,
570
- fill: 'both',
571
- });
572
- });
573
- observer.unobserve(container);
574
- }
575
- });
576
- }, { threshold: typeof threshold === 'number' ? threshold : threshold[0], rootMargin });
577
- observer.observe(container);
578
- return () => observer.disconnect();
579
- }, []);
580
- return ref;
581
- }
582
- return { useScrollAnimate, useScrollStagger };
583
- }
584
-
585
- /**
586
- * use-scroll-animate - Vue 3 Integration
587
- * Provides useScrollAnimate composable for Vue 3 applications.
588
- */
589
- function createVueComposables(Vue) {
590
- function useScrollAnimate(options = {}) {
591
- const animateRef = Vue.ref(null);
592
- let observer = null;
593
- let progressObserver = null;
594
- Vue.onMounted(() => {
595
- const el = animateRef.value;
596
- if (!el)
597
- return;
598
- const { animation = 'fade-in-up', duration = 600, delay = 0, easing = 'ease', threshold = 0.1, rootMargin = '0px', repeat = false, parallax = {}, onStart, onComplete, onEnter, onLeave, onProgress, } = options;
599
- const preset = resolvePreset(animation);
600
- const easingValue = resolveEasing(easing);
601
- let animated = false;
602
- el.style.opacity = '0';
603
- observer = new IntersectionObserver((entries) => {
604
- entries.forEach((entry) => {
605
- if (entry.isIntersecting) {
606
- onEnter === null || onEnter === void 0 ? void 0 : onEnter(el);
607
- if (!animated || repeat) {
608
- const anim = el.animate([preset.from, preset.to], {
609
- duration,
610
- delay,
611
- easing: easingValue,
612
- fill: 'both',
613
- });
614
- onStart === null || onStart === void 0 ? void 0 : onStart(el);
615
- anim.onfinish = () => onComplete === null || onComplete === void 0 ? void 0 : onComplete(el);
616
- animated = true;
617
- if (!repeat && !Object.keys(parallax).length && !onProgress) {
618
- observer === null || observer === void 0 ? void 0 : observer.unobserve(el);
619
- }
620
- }
621
- }
622
- else {
623
- onLeave === null || onLeave === void 0 ? void 0 : onLeave(el);
624
- if (repeat && animated) {
625
- el.style.opacity = '0';
626
- animated = false;
627
- }
628
- }
629
- });
630
- }, { threshold: typeof threshold === 'number' ? threshold : threshold[0], rootMargin });
631
- if (Object.keys(parallax).length > 0 || onProgress) {
632
- const thresholds = [];
633
- for (let i = 0; i <= 100; i++)
634
- thresholds.push(i / 100);
635
- progressObserver = new IntersectionObserver((entries) => {
636
- entries.forEach((entry) => {
637
- const progress = entry.intersectionRatio;
638
- onProgress === null || onProgress === void 0 ? void 0 : onProgress(el, progress);
639
- if (Object.keys(parallax).length > 0) {
640
- const { x = 0, y = 0, rotate = 0, scale = 1, speed = 1 } = parallax;
641
- const p = (progress - 0.5) * 2 * speed;
642
- let transform = '';
643
- if (x)
644
- transform += ` translateX(${typeof x === 'number' ? x * p + 'px' : 'calc(' + x + ' * ' + p + ')'})`;
645
- if (y)
646
- transform += ` translateY(${typeof y === 'number' ? y * p + 'px' : 'calc(' + y + ' * ' + p + ')'})`;
647
- if (rotate)
648
- transform += ` rotate(${rotate * p}deg)`;
649
- if (scale !== 1)
650
- transform += ` scale(${1 + (scale - 1) * p})`;
651
- el.style.transform = transform;
652
- }
653
- });
654
- }, { threshold: thresholds, rootMargin });
655
- progressObserver.observe(el);
656
- }
657
- observer.observe(el);
658
- });
659
- Vue.onUnmounted(() => {
660
- observer === null || observer === void 0 ? void 0 : observer.disconnect();
661
- progressObserver === null || progressObserver === void 0 ? void 0 : progressObserver.disconnect();
662
- });
663
- return { animateRef };
664
- }
665
- return { useScrollAnimate };
666
- }
667
-
668
- /**
669
- * use-scroll-animate
670
- *
671
- * A lightweight (~4KB gzipped), dependency-free scroll animation library
672
- * for modern web applications. Built with TypeScript, powered by
673
- * IntersectionObserver and the Web Animations API.
674
- *
675
- * @version 1.0.0
676
- * @license MIT
677
- * @see https://github.com/use-scroll-animate/use-scroll-animate
678
- */
679
- /**
680
- * Default singleton instance of ScrollAnimate.
681
- * Ready to use out of the box with sensible defaults.
682
- *
683
- * @example
684
- * ```js
685
- * import ScrollAnimate from 'use-scroll-animate';
686
- *
687
- * // Auto-initialize all elements with data-sa attribute
688
- * ScrollAnimate.init();
689
- *
690
- * // Or manually observe elements
691
- * ScrollAnimate.observe('.my-element', { animation: 'fade-in-up' });
692
- * ```
693
- */
694
- const ScrollAnimate = createScrollAnimate();
695
-
696
- export { EASING_MAP, PRESETS, createReactHooks, createScrollAnimate, createVueComposables, ScrollAnimate as default, resolveEasing, resolvePreset };
697
- //# sourceMappingURL=index.esm.js.map
1
+ /**
2
+ * use-scroll-animate - Animation Presets
3
+ * Defines keyframes for all built-in animation presets
4
+ */
5
+ const PRESETS = {
6
+ 'fade-in': {
7
+ from: { opacity: 0 },
8
+ to: { opacity: 1 },
9
+ },
10
+ 'fade-in-up': {
11
+ from: { opacity: 0, transform: 'translateY(40px)' },
12
+ to: { opacity: 1, transform: 'translateY(0px)' },
13
+ },
14
+ 'fade-in-down': {
15
+ from: { opacity: 0, transform: 'translateY(-40px)' },
16
+ to: { opacity: 1, transform: 'translateY(0px)' },
17
+ },
18
+ 'fade-in-left': {
19
+ from: { opacity: 0, transform: 'translateX(-40px)' },
20
+ to: { opacity: 1, transform: 'translateX(0px)' },
21
+ },
22
+ 'fade-in-right': {
23
+ from: { opacity: 0, transform: 'translateX(40px)' },
24
+ to: { opacity: 1, transform: 'translateX(0px)' },
25
+ },
26
+ 'zoom-in': {
27
+ from: { opacity: 0, transform: 'scale(0.8)' },
28
+ to: { opacity: 1, transform: 'scale(1)' },
29
+ },
30
+ 'zoom-out': {
31
+ from: { opacity: 0, transform: 'scale(1.2)' },
32
+ to: { opacity: 1, transform: 'scale(1)' },
33
+ },
34
+ 'flip-x': {
35
+ from: { opacity: 0, transform: 'rotateX(-90deg)' },
36
+ to: { opacity: 1, transform: 'rotateX(0deg)' },
37
+ },
38
+ 'flip-y': {
39
+ from: { opacity: 0, transform: 'rotateY(-90deg)' },
40
+ to: { opacity: 1, transform: 'rotateY(0deg)' },
41
+ },
42
+ 'slide-up': {
43
+ from: { transform: 'translateY(100%)' },
44
+ to: { transform: 'translateY(0px)' },
45
+ },
46
+ 'slide-down': {
47
+ from: { transform: 'translateY(-100%)' },
48
+ to: { transform: 'translateY(0px)' },
49
+ },
50
+ 'slide-left': {
51
+ from: { transform: 'translateX(-100%)' },
52
+ to: { transform: 'translateX(0px)' },
53
+ },
54
+ 'slide-right': {
55
+ from: { transform: 'translateX(100%)' },
56
+ to: { transform: 'translateX(0px)' },
57
+ },
58
+ 'bounce': {
59
+ from: { opacity: 0, transform: 'translateY(-60px)' },
60
+ to: { opacity: 1, transform: 'translateY(0px)' },
61
+ },
62
+ 'rotate-in': {
63
+ from: { opacity: 0, transform: 'rotate(-180deg) scale(0.5)' },
64
+ to: { opacity: 1, transform: 'rotate(0deg) scale(1)' },
65
+ },
66
+ 'blur-in': {
67
+ from: { opacity: 0, filter: 'blur(12px)' },
68
+ to: { opacity: 1, filter: 'blur(0px)' },
69
+ },
70
+ 'skew-in': {
71
+ from: { opacity: 0, transform: 'skewX(20deg) translateX(30px)' },
72
+ to: { opacity: 1, transform: 'skewX(0deg) translateX(0px)' },
73
+ },
74
+ 'scale-x': {
75
+ from: { transform: 'scaleX(0)' },
76
+ to: { transform: 'scaleX(1)' },
77
+ },
78
+ 'scale-y': {
79
+ from: { transform: 'scaleY(0)' },
80
+ to: { transform: 'scaleY(1)' },
81
+ },
82
+ 'shimmer': {
83
+ from: { opacity: 0.5, filter: 'brightness(1)' },
84
+ to: { opacity: 1, filter: 'brightness(1.5)' },
85
+ },
86
+ 'pulse': {
87
+ from: { transform: 'scale(1)' },
88
+ to: { transform: 'scale(1.05)' },
89
+ },
90
+ 'swing': {
91
+ from: { transform: 'rotate(-10deg)' },
92
+ to: { transform: 'rotate(10deg)' },
93
+ },
94
+ 'scale-up': {
95
+ from: { opacity: 0, transform: 'scale(0.5)' },
96
+ to: { opacity: 1, transform: 'scale(1)' },
97
+ },
98
+ 'blur-in-up': {
99
+ from: { opacity: 0, filter: 'blur(12px)', transform: 'translateY(40px)' },
100
+ to: { opacity: 1, filter: 'blur(0px)', transform: 'translateY(0px)' },
101
+ },
102
+ 'flip-up': {
103
+ from: { opacity: 0, transform: 'perspective(800px) rotateX(60deg)' },
104
+ to: { opacity: 1, transform: 'perspective(800px) rotateX(0deg)' },
105
+ },
106
+ 'flip-down': {
107
+ from: { opacity: 0, transform: 'perspective(800px) rotateX(-60deg)' },
108
+ to: { opacity: 1, transform: 'perspective(800px) rotateX(0deg)' },
109
+ },
110
+ 'rotate-left': {
111
+ from: { opacity: 0, transform: 'rotate(-15deg) translateX(-40px)' },
112
+ to: { opacity: 1, transform: 'rotate(0deg) translateX(0px)' },
113
+ },
114
+ 'rotate-right': {
115
+ from: { opacity: 0, transform: 'rotate(15deg) translateX(40px)' },
116
+ to: { opacity: 1, transform: 'rotate(0deg) translateX(0px)' },
117
+ },
118
+ // clip-path reveals: content is uncovered without moving or fading
119
+ 'clip-up': {
120
+ from: { clipPath: 'inset(100% 0% 0% 0%)' },
121
+ to: { clipPath: 'inset(0% 0% 0% 0%)' },
122
+ },
123
+ 'clip-down': {
124
+ from: { clipPath: 'inset(0% 0% 100% 0%)' },
125
+ to: { clipPath: 'inset(0% 0% 0% 0%)' },
126
+ },
127
+ 'clip-left': {
128
+ from: { clipPath: 'inset(0% 0% 0% 100%)' },
129
+ to: { clipPath: 'inset(0% 0% 0% 0%)' },
130
+ },
131
+ 'clip-right': {
132
+ from: { clipPath: 'inset(0% 100% 0% 0%)' },
133
+ to: { clipPath: 'inset(0% 0% 0% 0%)' },
134
+ },
135
+ 'clip-circle': {
136
+ from: { clipPath: 'circle(0% at 50% 50%)' },
137
+ to: { clipPath: 'circle(75% at 50% 50%)' },
138
+ },
139
+ };
140
+ function resolvePreset(animation) {
141
+ var _a;
142
+ if (typeof animation === 'string') {
143
+ return (_a = PRESETS[animation.trim()]) !== null && _a !== void 0 ? _a : PRESETS['fade-in-up'];
144
+ }
145
+ if (Array.isArray(animation)) {
146
+ const combined = { from: {}, to: {} };
147
+ animation.forEach(name => {
148
+ const preset = PRESETS[name.trim()];
149
+ if (preset) {
150
+ Object.entries(preset.from).forEach(([key, val]) => {
151
+ if (key === 'transform' && combined.from[key]) {
152
+ combined.from[key] = `${combined.from[key]} ${val}`;
153
+ }
154
+ else {
155
+ combined.from[key] = val;
156
+ }
157
+ });
158
+ Object.entries(preset.to).forEach(([key, val]) => {
159
+ if (key === 'transform' && combined.to[key]) {
160
+ combined.to[key] = `${combined.to[key]} ${val}`;
161
+ }
162
+ else {
163
+ combined.to[key] = val;
164
+ }
165
+ });
166
+ }
167
+ });
168
+ return combined;
169
+ }
170
+ return animation;
171
+ }
172
+ /** Easing to CSS cubic-bezier mapping */
173
+ const EASING_MAP = {
174
+ linear: 'linear',
175
+ ease: 'ease',
176
+ 'ease-in': 'ease-in',
177
+ 'ease-out': 'ease-out',
178
+ 'ease-in-out': 'ease-in-out',
179
+ spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
180
+ 'soft-spring': 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
181
+ 'heavy-bounce': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
182
+ };
183
+ function resolveEasing(easing) {
184
+ var _a;
185
+ if (typeof easing === 'string') {
186
+ return (_a = EASING_MAP[easing]) !== null && _a !== void 0 ? _a : easing;
187
+ }
188
+ if (Array.isArray(easing)) {
189
+ return `cubic-bezier(${easing.join(', ')})`;
190
+ }
191
+ if (typeof easing === 'function') {
192
+ // Functions cannot be expressed as a CSS easing string; the core samples
193
+ // them into a `linear()` easing (or keyframes on older browsers).
194
+ return 'linear';
195
+ }
196
+ return 'ease';
197
+ }
198
+
199
+ /**
200
+ * use-scroll-animate - Core Implementation
201
+ * Uses IntersectionObserver + Web Animations API for zero-dependency,
202
+ * high-performance scroll-triggered animations.
203
+ */
204
+ const DEFAULT_CONFIG = {
205
+ defaultAnimation: 'fade-in-up',
206
+ defaultDuration: 600,
207
+ defaultDelay: 0,
208
+ defaultEasing: 'ease',
209
+ defaultThreshold: 0.1,
210
+ defaultRootMargin: '0px',
211
+ defaultRepeat: false,
212
+ defaultOnce: true,
213
+ defaultOffset: 0,
214
+ hiddenClass: 'sa-hidden',
215
+ visibleClass: 'sa-visible',
216
+ useClassNames: false,
217
+ disabled: false,
218
+ root: null,
219
+ autoUnregister: true,
220
+ };
221
+ const noop = () => undefined;
222
+ /* ------------------------------------------------------------------ */
223
+ /* Environment helpers (all SSR-safe: never touch globals at import) */
224
+ /* ------------------------------------------------------------------ */
225
+ const hasDOM = () => typeof window !== 'undefined' && typeof document !== 'undefined';
226
+ /** @internal */
227
+ const supportsObserver = () => hasDOM() && typeof IntersectionObserver !== 'undefined';
228
+ let reducedMotionQuery;
229
+ /** @internal Whether the user asked the OS/browser to reduce motion. */
230
+ function prefersReducedMotion() {
231
+ if (!hasDOM())
232
+ return false;
233
+ if (reducedMotionQuery === undefined) {
234
+ reducedMotionQuery =
235
+ typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
236
+ }
237
+ return !!reducedMotionQuery && reducedMotionQuery.matches;
238
+ }
239
+ /* ------------------------------------------------------------------ */
240
+ /* Option parsing */
241
+ /* ------------------------------------------------------------------ */
242
+ function num(value) {
243
+ if (value === undefined || value.trim() === '')
244
+ return undefined;
245
+ const n = parseFloat(value);
246
+ return Number.isFinite(n) ? n : undefined;
247
+ }
248
+ /** Numeric strings ("100") become numbers (px); strings with units stay as-is. */
249
+ function lengthValue(value) {
250
+ if (value === undefined || value.trim() === '')
251
+ return undefined;
252
+ const v = value.trim();
253
+ return /^-?(\d+\.?\d*|\.\d+)$/.test(v) ? parseFloat(v) : v;
254
+ }
255
+ function parseDataAttributes(el, config) {
256
+ var _a;
257
+ const dataset = el.dataset || {};
258
+ const opts = {};
259
+ if (dataset.saAnimation) {
260
+ const anim = dataset.saAnimation;
261
+ opts.animation = (anim.includes(',') ? anim.split(',').map((s) => s.trim()) : anim.trim());
262
+ }
263
+ opts.duration = num(dataset.saDuration);
264
+ opts.delay = num(dataset.saDelay);
265
+ if (dataset.saEasing) {
266
+ const e = dataset.saEasing.trim();
267
+ if (e.startsWith('[')) {
268
+ try {
269
+ opts.easing = JSON.parse(e);
270
+ }
271
+ catch (_b) {
272
+ // Malformed JSON: fall back to the default easing instead of throwing
273
+ }
274
+ }
275
+ else {
276
+ opts.easing = e;
277
+ }
278
+ }
279
+ if (dataset.saThreshold) {
280
+ const t = dataset.saThreshold;
281
+ opts.threshold = t.includes(',')
282
+ ? t.split(',').map(parseFloat).filter(Number.isFinite)
283
+ : num(t);
284
+ }
285
+ if (dataset.saRootMargin)
286
+ opts.rootMargin = dataset.saRootMargin;
287
+ if (dataset.saRepeat !== undefined)
288
+ opts.repeat = dataset.saRepeat !== 'false';
289
+ if (dataset.saOnce !== undefined)
290
+ opts.once = dataset.saOnce !== 'false';
291
+ opts.offset = num(dataset.saOffset);
292
+ opts.stagger = num(dataset.saStagger);
293
+ if (dataset.saProgress)
294
+ opts.progressMode = dataset.saProgress.trim() === 'scroll' ? 'scroll' : 'ratio';
295
+ if (dataset.saParallaxX || dataset.saParallaxY || dataset.saParallaxRotate || dataset.saParallaxScale) {
296
+ opts.parallax = {
297
+ x: lengthValue(dataset.saParallaxX),
298
+ y: lengthValue(dataset.saParallaxY),
299
+ rotate: num(dataset.saParallaxRotate),
300
+ scale: num(dataset.saParallaxScale),
301
+ speed: (_a = num(dataset.saParallaxSpeed)) !== null && _a !== void 0 ? _a : 1,
302
+ };
303
+ }
304
+ return mergeOptions(opts, config);
305
+ }
306
+ function mergeOptions(opts, config) {
307
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
308
+ const repeat = (_a = opts.repeat) !== null && _a !== void 0 ? _a : config.defaultRepeat;
309
+ const threshold = (_b = opts.threshold) !== null && _b !== void 0 ? _b : config.defaultThreshold;
310
+ return {
311
+ animation: (_c = opts.animation) !== null && _c !== void 0 ? _c : config.defaultAnimation,
312
+ duration: (_d = opts.duration) !== null && _d !== void 0 ? _d : config.defaultDuration,
313
+ delay: (_e = opts.delay) !== null && _e !== void 0 ? _e : config.defaultDelay,
314
+ easing: (_f = opts.easing) !== null && _f !== void 0 ? _f : config.defaultEasing,
315
+ threshold: Array.isArray(threshold) && threshold.length === 0 ? config.defaultThreshold : threshold,
316
+ rootMargin: (_g = opts.rootMargin) !== null && _g !== void 0 ? _g : config.defaultRootMargin,
317
+ repeat,
318
+ once: (_h = opts.once) !== null && _h !== void 0 ? _h : (repeat ? false : config.defaultOnce),
319
+ offset: (_j = opts.offset) !== null && _j !== void 0 ? _j : config.defaultOffset,
320
+ stagger: (_k = opts.stagger) !== null && _k !== void 0 ? _k : 0,
321
+ parallax: (_l = opts.parallax) !== null && _l !== void 0 ? _l : {},
322
+ onStart: (_m = opts.onStart) !== null && _m !== void 0 ? _m : noop,
323
+ onComplete: (_o = opts.onComplete) !== null && _o !== void 0 ? _o : noop,
324
+ onEnter: (_p = opts.onEnter) !== null && _p !== void 0 ? _p : noop,
325
+ onLeave: (_q = opts.onLeave) !== null && _q !== void 0 ? _q : noop,
326
+ onProgress: (_r = opts.onProgress) !== null && _r !== void 0 ? _r : noop,
327
+ progressMode: (_s = opts.progressMode) !== null && _s !== void 0 ? _s : 'ratio',
328
+ };
329
+ }
330
+ function resolveTargets(target) {
331
+ if (!target || !hasDOM())
332
+ return [];
333
+ if (typeof target === 'string')
334
+ return Array.from(document.querySelectorAll(target));
335
+ if (target instanceof Element)
336
+ return [target];
337
+ if (Array.isArray(target) || target instanceof NodeList) {
338
+ return Array.from(target).filter((el) => el instanceof Element);
339
+ }
340
+ return [];
341
+ }
342
+ /**
343
+ * Apply `offset` to the bottom edge of a rootMargin, preserving the other
344
+ * three sides. `offset: 100` means "trigger 100px later" (bottom -100px).
345
+ * @internal
346
+ */
347
+ function applyOffset(rootMargin, offset) {
348
+ var _a, _b, _c;
349
+ if (!offset)
350
+ return rootMargin;
351
+ const parts = (rootMargin || '0px').trim().split(/\s+/);
352
+ const top = parts[0];
353
+ const right = (_a = parts[1]) !== null && _a !== void 0 ? _a : top;
354
+ const bottom = (_b = parts[2]) !== null && _b !== void 0 ? _b : top;
355
+ const left = (_c = parts[3]) !== null && _c !== void 0 ? _c : right;
356
+ const m = /^(-?\d*\.?\d+)(px)?$/.exec(bottom);
357
+ const base = m ? parseFloat(m[1]) : 0; // non-px bottoms (e.g. %) cannot be combined; offset wins
358
+ return `${top} ${right} ${base - offset}px ${left}`;
359
+ }
360
+ function hasParallax(p) {
361
+ return !!p && Object.keys(p).some((k) => p[k] !== undefined);
362
+ }
363
+ function needsProgress(opts) {
364
+ return hasParallax(opts.parallax) || opts.onProgress !== noop;
365
+ }
366
+ /**
367
+ * True scroll progress of `el` through the viewport (or `root`): 0 when its top
368
+ * edge reaches the bottom of the viewport, 1 when its bottom edge passes the top.
369
+ * Works for elements taller than the viewport. Returns 0 without a DOM.
370
+ */
371
+ function getScrollProgress(el, root) {
372
+ if (!hasDOM())
373
+ return 0;
374
+ const rect = el.getBoundingClientRect();
375
+ let top = 0;
376
+ let height = window.innerHeight || document.documentElement.clientHeight || 0;
377
+ if (root) {
378
+ const r = root.getBoundingClientRect();
379
+ top = r.top;
380
+ height = r.height;
381
+ }
382
+ const total = height + rect.height;
383
+ if (total <= 0)
384
+ return 0;
385
+ const p = (top + height - rect.top) / total;
386
+ return p < 0 ? 0 : p > 1 ? 1 : p;
387
+ }
388
+ /* ------------------------------------------------------------------ */
389
+ /* Animation */
390
+ /* ------------------------------------------------------------------ */
391
+ /** The animation currently running on an element, so it can be cancelled/replaced. */
392
+ const running = new WeakMap();
393
+ const timers = new WeakMap();
394
+ function cancelRunning(el) {
395
+ const anim = running.get(el);
396
+ if (anim) {
397
+ running.delete(el);
398
+ anim.onfinish = null;
399
+ anim.cancel();
400
+ }
401
+ const timer = timers.get(el);
402
+ if (timer !== undefined) {
403
+ clearTimeout(timer);
404
+ timers.delete(el);
405
+ }
406
+ }
407
+ function setStyles(el, styles) {
408
+ const style = el.style;
409
+ if (!style)
410
+ return;
411
+ Object.keys(styles).forEach((prop) => {
412
+ style[prop] = String(styles[prop]);
413
+ });
414
+ }
415
+ const NUM_UNIT = /(-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)/gi;
416
+ /**
417
+ * Interpolate two CSS values at `t`. Numbers interpolate directly; strings
418
+ * interpolate when they share the same structure (e.g. `translateY(40px)` ->
419
+ * `translateY(0px)`, or `scale(0.8)` -> `scale(1)`). Otherwise snaps at 0.5.
420
+ * @internal
421
+ */
422
+ function interpolateValue(from, to, t) {
423
+ if (typeof from === 'number' && typeof to === 'number')
424
+ return from + (to - from) * t;
425
+ const a = String(from);
426
+ const b = String(to);
427
+ const ta = [];
428
+ const tb = [];
429
+ const skA = a.replace(NUM_UNIT, (_, n, u) => (ta.push([parseFloat(n), u]), '#'));
430
+ const skB = b.replace(NUM_UNIT, (_, n, u) => (tb.push([parseFloat(n), u]), '#'));
431
+ if (skA !== skB || ta.length !== tb.length)
432
+ return t < 0.5 ? from : to;
433
+ let i = 0;
434
+ let ok = true;
435
+ const out = skA.replace(/#/g, () => {
436
+ const [na, ua] = ta[i];
437
+ const [nb, ub] = tb[i++];
438
+ if (ua !== ub && na !== 0 && nb !== 0 && ua && ub)
439
+ ok = false;
440
+ const unit = ua || ub;
441
+ return `${+(na + (nb - na) * t).toFixed(4)}${unit}`;
442
+ });
443
+ return ok ? out : t < 0.5 ? from : to;
444
+ }
445
+ const EASING_SAMPLES = 30;
446
+ let linearSupported;
447
+ function supportsLinearEasing() {
448
+ if (linearSupported === undefined) {
449
+ linearSupported =
450
+ typeof CSS !== 'undefined' &&
451
+ typeof CSS.supports === 'function' &&
452
+ CSS.supports('animation-timing-function', 'linear(0, 1)');
453
+ }
454
+ return linearSupported;
455
+ }
456
+ function sampleEasing(fn) {
457
+ const values = [];
458
+ for (let i = 0; i <= EASING_SAMPLES; i++) {
459
+ const v = fn(i / EASING_SAMPLES);
460
+ values.push(Number.isFinite(v) ? +v.toFixed(4) : i / EASING_SAMPLES);
461
+ }
462
+ return values;
463
+ }
464
+ function buildAnimation(preset, easing) {
465
+ if (typeof easing !== 'function') {
466
+ return { keyframes: [preset.from, preset.to], easing: resolveEasing(easing) };
467
+ }
468
+ const samples = sampleEasing(easing);
469
+ // Modern browsers: an exact, property-agnostic `linear()` easing curve.
470
+ if (supportsLinearEasing()) {
471
+ return { keyframes: [preset.from, preset.to], easing: `linear(${samples.join(', ')})` };
472
+ }
473
+ // Fallback: approximate the curve with interpolated keyframes.
474
+ const props = Object.keys(preset.from).filter((p) => p in preset.to);
475
+ const keyframes = samples.map((eased, i) => {
476
+ const frame = { offset: i / EASING_SAMPLES };
477
+ props.forEach((prop) => {
478
+ frame[prop] = interpolateValue(preset.from[prop], preset.to[prop], eased);
479
+ });
480
+ return frame;
481
+ });
482
+ return { keyframes, easing: 'linear' };
483
+ }
484
+ /** Make an element visible without animating (reduced motion / disabled / no WAAPI). */
485
+ function reveal(el, config) {
486
+ cancelRunning(el);
487
+ if (config.useClassNames) {
488
+ el.classList.remove(config.hiddenClass);
489
+ el.classList.add(config.visibleClass);
490
+ }
491
+ else {
492
+ const style = el.style;
493
+ if (style)
494
+ style.opacity = '';
495
+ }
496
+ }
497
+ /** @internal Cancel a running/pending animation and make the element visible. */
498
+ function stopAnimation(el, config = {}) {
499
+ reveal(el, { ...DEFAULT_CONFIG, ...config });
500
+ }
501
+ /** @internal Whether animations should be skipped entirely. */
502
+ function motionDisabled(config) {
503
+ return !!config.disabled || prefersReducedMotion();
504
+ }
505
+ function runAnimation(el, opts, config, staggerIndex = 0) {
506
+ const { duration, delay, stagger, onStart, onComplete } = opts;
507
+ const totalDelay = Math.max(0, delay + staggerIndex * stagger);
508
+ cancelRunning(el);
509
+ if (motionDisabled(config)) {
510
+ reveal(el, config);
511
+ onStart(el);
512
+ onComplete(el);
513
+ return;
514
+ }
515
+ if (config.useClassNames) {
516
+ if (totalDelay > 0)
517
+ el.style.animationDelay = `${totalDelay}ms`;
518
+ el.classList.remove(config.hiddenClass);
519
+ el.classList.add(config.visibleClass);
520
+ onStart(el);
521
+ timers.set(el, setTimeout(() => {
522
+ timers.delete(el);
523
+ onComplete(el);
524
+ }, duration + totalDelay));
525
+ return;
526
+ }
527
+ const preset = resolvePreset(opts.animation);
528
+ // Presets that don't animate opacity (slide-*, clip-*, scale-x, ...) would
529
+ // otherwise stay at the `opacity: 0` applied while waiting to enter.
530
+ if (!('opacity' in preset.to)) {
531
+ const style = el.style;
532
+ if (style && style.opacity === '0')
533
+ style.opacity = '';
534
+ }
535
+ if (typeof el.animate !== 'function') {
536
+ setStyles(el, preset.to);
537
+ onStart(el);
538
+ onComplete(el);
539
+ return;
540
+ }
541
+ const built = buildAnimation(preset, opts.easing);
542
+ const timing = { duration, delay: totalDelay, easing: built.easing, fill: 'both' };
543
+ let anim;
544
+ try {
545
+ anim = el.animate(built.keyframes, timing);
546
+ }
547
+ catch (_a) {
548
+ // Invalid user easing string (WAAPI throws a TypeError): fall back to 'ease'
549
+ anim = el.animate(built.keyframes, { ...timing, easing: 'ease' });
550
+ }
551
+ running.set(el, anim);
552
+ onStart(el);
553
+ anim.onfinish = () => {
554
+ if (running.get(el) === anim) {
555
+ running.delete(el);
556
+ // Persist the end state inline and drop the filling animation. This frees
557
+ // the Animation object and lets later inline styles (parallax, user code)
558
+ // take effect instead of being masked by `fill: forwards`.
559
+ try {
560
+ anim.commitStyles();
561
+ }
562
+ catch (_a) {
563
+ setStyles(el, preset.to);
564
+ }
565
+ anim.cancel();
566
+ }
567
+ onComplete(el);
568
+ };
569
+ }
570
+ function applyParallax(el, progress, parallax) {
571
+ const { x = 0, y = 0, rotate = 0, scale = 1, speed = 1 } = parallax;
572
+ const p = (progress - 0.5) * 2 * speed;
573
+ const axis = (v) => (typeof v === 'number' ? `${v * p}px` : `calc(${v} * ${p})`);
574
+ let transform = '';
575
+ if (x)
576
+ transform += ` translateX(${axis(x)})`;
577
+ if (y)
578
+ transform += ` translateY(${axis(y)})`;
579
+ if (rotate)
580
+ transform += ` rotate(${rotate * p}deg)`;
581
+ if (scale !== 1)
582
+ transform += ` scale(${1 + (scale - 1) * p})`;
583
+ el.style.transform = transform.trim();
584
+ }
585
+ function hideElement(el, config) {
586
+ cancelRunning(el);
587
+ if (config.useClassNames) {
588
+ el.classList.add(config.hiddenClass);
589
+ el.classList.remove(config.visibleClass);
590
+ }
591
+ else {
592
+ const style = el.style;
593
+ if (style)
594
+ style.opacity = '0';
595
+ }
596
+ }
597
+ /** @internal Hide an element before its entrance animation, unless motion is off. */
598
+ function prepareElement(el, config = {}) {
599
+ const full = { ...DEFAULT_CONFIG, ...config };
600
+ if (!motionDisabled(full))
601
+ hideElement(el, full);
602
+ }
603
+ let progressThresholds;
604
+ function getProgressThresholds() {
605
+ if (!progressThresholds) {
606
+ progressThresholds = [];
607
+ for (let i = 0; i <= 100; i++)
608
+ progressThresholds.push(i / 100);
609
+ }
610
+ return progressThresholds;
611
+ }
612
+ const PASSIVE = { passive: true };
613
+ /* ------------------------------------------------------------------ */
614
+ /* Instance */
615
+ /* ------------------------------------------------------------------ */
616
+ function createScrollAnimate(userConfig = {}) {
617
+ let config = { ...DEFAULT_CONFIG, ...userConfig };
618
+ const registry = new Map();
619
+ // `once` elements that finished and were dropped from the registry (autoUnregister).
620
+ let finished = new WeakSet();
621
+ // Elements in `progressMode: 'scroll'` that are currently inside the viewport.
622
+ const scrolling = new Set();
623
+ let frame = 0;
624
+ let listening = null;
625
+ // Observers are shared between elements with the same root/threshold/rootMargin,
626
+ // instead of one (or two) IntersectionObservers per element.
627
+ const pools = new Map();
628
+ function pooled(root, key, create) {
629
+ let pool = pools.get(root);
630
+ if (!pool)
631
+ pools.set(root, (pool = new Map()));
632
+ let io = pool.get(key);
633
+ if (!io)
634
+ pool.set(key, (io = create()));
635
+ return io;
636
+ }
637
+ function teardown(el, restore) {
638
+ var _a;
639
+ const record = registry.get(el);
640
+ if (!record)
641
+ return;
642
+ record.observer.unobserve(el);
643
+ (_a = record.progressObserver) === null || _a === void 0 ? void 0 : _a.unobserve(el);
644
+ registry.delete(el);
645
+ untrack(el);
646
+ // An element that never animated would otherwise stay invisible forever.
647
+ if (restore && !record.animated)
648
+ reveal(el, config);
649
+ }
650
+ function pruneDetached() {
651
+ registry.forEach((record, el) => {
652
+ if (el.isConnected === false)
653
+ teardown(el, false);
654
+ });
655
+ }
656
+ function emitProgress(el, record, progress) {
657
+ const opts = record.options;
658
+ opts.onProgress(el, progress);
659
+ if (hasParallax(opts.parallax) && !motionDisabled(config))
660
+ applyParallax(el, progress, opts.parallax);
661
+ }
662
+ function update() {
663
+ frame = 0;
664
+ scrolling.forEach((el) => {
665
+ const record = registry.get(el);
666
+ if (record)
667
+ emitProgress(el, record, getScrollProgress(el, config.root));
668
+ });
669
+ }
670
+ // rAF-throttled; IntersectionObserver-capable browsers all have rAF.
671
+ const schedule = () => frame || (frame = requestAnimationFrame(update));
672
+ function listen(on) {
673
+ if (on === !!listening)
674
+ return;
675
+ const method = on ? 'addEventListener' : 'removeEventListener';
676
+ const target = listening || config.root || window;
677
+ target[method]('scroll', schedule, PASSIVE);
678
+ window[method]('resize', schedule, PASSIVE);
679
+ listening = on ? target : null;
680
+ if (!on && frame) {
681
+ cancelAnimationFrame(frame);
682
+ frame = 0;
683
+ }
684
+ }
685
+ function untrack(el) {
686
+ if (scrolling.delete(el) && !scrolling.size)
687
+ listen(false);
688
+ }
689
+ function onScrollIntersect(entries) {
690
+ entries.forEach((entry) => {
691
+ const el = entry.target;
692
+ const record = registry.get(el);
693
+ if (!record)
694
+ return;
695
+ if (entry.isIntersecting) {
696
+ scrolling.add(el);
697
+ listen(true);
698
+ }
699
+ else {
700
+ untrack(el);
701
+ }
702
+ // Emit right away so the edges (0 / 1) are reported even on fast scrolls.
703
+ emitProgress(el, record, getScrollProgress(el, config.root));
704
+ });
705
+ }
706
+ function onIntersect(entries) {
707
+ const staggerCounts = new Map();
708
+ entries.forEach((entry) => {
709
+ var _a;
710
+ const el = entry.target;
711
+ const record = registry.get(el);
712
+ if (!record)
713
+ return;
714
+ const opts = record.options;
715
+ if (el.isConnected === false) {
716
+ teardown(el, false);
717
+ return;
718
+ }
719
+ if (entry.isIntersecting) {
720
+ opts.onEnter(el);
721
+ if (record.animated && !opts.repeat)
722
+ return;
723
+ // Stagger relative to siblings revealed in the same batch, so elements
724
+ // scrolled into view later don't inherit an ever-growing delay.
725
+ let staggerIndex = 0;
726
+ if (opts.stagger > 0) {
727
+ const parent = el.parentElement;
728
+ staggerIndex = (_a = staggerCounts.get(parent)) !== null && _a !== void 0 ? _a : 0;
729
+ staggerCounts.set(parent, staggerIndex + 1);
730
+ }
731
+ runAnimation(el, opts, config, staggerIndex);
732
+ record.animated = true;
733
+ if (opts.once && !opts.repeat) {
734
+ // Keep the progress observer: parallax/onProgress must keep working.
735
+ record.observer.unobserve(el);
736
+ if (config.autoUnregister && !record.progressObserver) {
737
+ // Nothing left to watch: free the record (the running animation
738
+ // keeps its own reference until it finishes).
739
+ finished.add(el);
740
+ teardown(el, false);
741
+ }
742
+ }
743
+ }
744
+ else {
745
+ opts.onLeave(el);
746
+ if (opts.repeat && record.animated) {
747
+ if (!motionDisabled(config))
748
+ hideElement(el, config);
749
+ record.animated = false;
750
+ }
751
+ }
752
+ });
753
+ }
754
+ function onProgress(entries) {
755
+ entries.forEach((entry) => {
756
+ const record = registry.get(entry.target);
757
+ if (!record)
758
+ return;
759
+ emitProgress(entry.target, record, entry.intersectionRatio);
760
+ });
761
+ }
762
+ function getObserver(opts) {
763
+ const rootMargin = applyOffset(opts.rootMargin, opts.offset);
764
+ const threshold = opts.threshold;
765
+ const root = config.root;
766
+ return pooled(root, `m|${rootMargin}|${String(threshold)}`, () => new IntersectionObserver(onIntersect, { threshold, rootMargin, root: root }));
767
+ }
768
+ function getProgressObserver(opts) {
769
+ const root = config.root;
770
+ if (opts.progressMode === 'scroll') {
771
+ // Only used to know when to start/stop measuring; progress itself comes
772
+ // from a single shared, rAF-throttled passive scroll listener.
773
+ return pooled(root, `s|${opts.rootMargin}`, () => new IntersectionObserver(onScrollIntersect, { threshold: 0, rootMargin: opts.rootMargin, root: root }));
774
+ }
775
+ return pooled(root, `p|${opts.rootMargin}`, () => new IntersectionObserver(onProgress, {
776
+ threshold: getProgressThresholds(),
777
+ rootMargin: opts.rootMargin,
778
+ root: root,
779
+ }));
780
+ }
781
+ function attach(el, record, observeMain) {
782
+ var _a;
783
+ record.observer = getObserver(record.options);
784
+ if (observeMain)
785
+ record.observer.observe(el);
786
+ record.progressObserver = needsProgress(record.options) ? getProgressObserver(record.options) : undefined;
787
+ (_a = record.progressObserver) === null || _a === void 0 ? void 0 : _a.observe(el);
788
+ }
789
+ function observeElement(el, opts) {
790
+ if (registry.has(el) || finished.has(el))
791
+ return;
792
+ if (!supportsObserver()) {
793
+ // No IntersectionObserver (very old browser): never leave content hidden.
794
+ reveal(el, config);
795
+ return;
796
+ }
797
+ if (!motionDisabled(config))
798
+ hideElement(el, config);
799
+ const record = { element: el, options: opts, animated: false };
800
+ registry.set(el, record);
801
+ attach(el, record, true);
802
+ }
803
+ const instance = {
804
+ observe(target, options = {}) {
805
+ pruneDetached();
806
+ const opts = mergeOptions(options, config);
807
+ resolveTargets(target).forEach((el) => observeElement(el, opts));
808
+ },
809
+ unobserve(target) {
810
+ resolveTargets(target).forEach((el) => {
811
+ finished.delete(el);
812
+ teardown(el, true);
813
+ });
814
+ },
815
+ init(rootElement) {
816
+ const scope = rootElement !== null && rootElement !== void 0 ? rootElement : (hasDOM() ? document : null);
817
+ if (!scope)
818
+ return;
819
+ pruneDetached();
820
+ scope.querySelectorAll('[data-sa]').forEach((el) => {
821
+ if (!registry.has(el))
822
+ observeElement(el, parseDataAttributes(el, config));
823
+ });
824
+ },
825
+ destroy() {
826
+ registry.forEach((record, el) => {
827
+ if (!record.animated)
828
+ reveal(el, config);
829
+ });
830
+ pools.forEach((pool) => pool.forEach((io) => io.disconnect()));
831
+ pools.clear();
832
+ registry.clear();
833
+ scrolling.clear();
834
+ listen(false);
835
+ finished = new WeakSet();
836
+ },
837
+ refresh() {
838
+ // Rebuild observers (e.g. after configure({ root })) without re-hiding or
839
+ // replaying elements that have already animated.
840
+ pools.forEach((pool) => pool.forEach((io) => io.disconnect()));
841
+ pools.clear();
842
+ scrolling.clear();
843
+ listen(false);
844
+ pruneDetached();
845
+ if (!supportsObserver())
846
+ return;
847
+ registry.forEach((record, el) => {
848
+ const done = record.animated && record.options.once && !record.options.repeat;
849
+ attach(el, record, !done);
850
+ });
851
+ },
852
+ animate(target, options = {}) {
853
+ const opts = mergeOptions(options, config);
854
+ resolveTargets(target).forEach((el) => runAnimation(el, opts, config, 0));
855
+ },
856
+ getObservedElements() {
857
+ return Array.from(registry.values());
858
+ },
859
+ configure(newConfig) {
860
+ config = { ...config, ...newConfig };
861
+ },
862
+ };
863
+ return instance;
864
+ }
865
+
866
+ /**
867
+ * use-scroll-animate - Staggered children
868
+ * Reveal a container's children one after another when the container scrolls
869
+ * into view, optionally also animating children that are added later.
870
+ */
871
+ let fallback$1 = null;
872
+ /**
873
+ * Animate the children of `container` with a stagger once it enters the
874
+ * viewport. Returns a cleanup function. SSR-safe (no-op without a DOM).
875
+ *
876
+ * @example
877
+ * const stop = staggerChildren(document.querySelector('ul'), { stagger: 60, observeChildren: true });
878
+ */
879
+ function staggerChildren(container, options = {}, instance) {
880
+ if (!container || !hasDOM() || !supportsObserver())
881
+ return () => undefined; // leave content visible
882
+ const sa = instance || fallback$1 || (fallback$1 = createScrollAnimate());
883
+ const { stagger = 80, delay = 0, threshold = 0.1, rootMargin = '0px', observeChildren = false, ...rest } = options;
884
+ let items = Array.from(container.children);
885
+ let revealed = false;
886
+ const late = [];
887
+ items.forEach((child) => prepareElement(child));
888
+ const io = new IntersectionObserver((entries) => {
889
+ if (revealed || !entries.some((entry) => entry.isIntersecting))
890
+ return;
891
+ revealed = true;
892
+ io.disconnect();
893
+ items.forEach((child, i) => {
894
+ if (child.parentNode === container)
895
+ sa.animate(child, { ...rest, delay: delay + i * stagger });
896
+ });
897
+ items = [];
898
+ }, { threshold, rootMargin });
899
+ io.observe(container);
900
+ let mo;
901
+ if (observeChildren && typeof MutationObserver !== 'undefined') {
902
+ mo = new MutationObserver((records) => {
903
+ records.forEach((record) => {
904
+ record.addedNodes.forEach((node) => {
905
+ if (!(node instanceof Element) || node.parentNode !== container)
906
+ return;
907
+ if (!revealed) {
908
+ prepareElement(node);
909
+ items.push(node);
910
+ }
911
+ else {
912
+ // The core engine staggers siblings relative to the batch that
913
+ // enters the viewport together.
914
+ late.push(node);
915
+ sa.observe(node, { ...rest, delay, stagger, threshold, rootMargin });
916
+ }
917
+ });
918
+ record.removedNodes.forEach((node) => {
919
+ if (!(node instanceof Element))
920
+ return;
921
+ items = items.filter((el) => el !== node);
922
+ const i = late.indexOf(node);
923
+ if (i >= 0) {
924
+ late.splice(i, 1);
925
+ sa.unobserve(node);
926
+ }
927
+ });
928
+ });
929
+ });
930
+ mo.observe(container, { childList: true });
931
+ }
932
+ return () => {
933
+ io.disconnect();
934
+ mo === null || mo === void 0 ? void 0 : mo.disconnect();
935
+ late.forEach((el) => sa.unobserve(el));
936
+ late.length = 0;
937
+ };
938
+ }
939
+
940
+ /**
941
+ * use-scroll-animate - Sequence / timeline helper
942
+ * Chain animations on several targets, one after another (or overlapping).
943
+ */
944
+ let fallback = null;
945
+ function plan(steps, defaults) {
946
+ const out = [];
947
+ let cursor = 0;
948
+ steps.forEach((step) => {
949
+ var _a, _b;
950
+ const { target, gap = 0, at, ...stepOpts } = step;
951
+ const opts = { ...defaults, ...stepOpts };
952
+ const duration = (_a = opts.duration) !== null && _a !== void 0 ? _a : 600;
953
+ const start = Math.max(0, at !== null && at !== void 0 ? at : cursor + gap) + ((_b = opts.delay) !== null && _b !== void 0 ? _b : 0);
954
+ let end = Math.max(cursor, start);
955
+ resolveTargets(target).forEach((el, i) => {
956
+ var _a;
957
+ const delay = start + i * ((_a = opts.stagger) !== null && _a !== void 0 ? _a : 0);
958
+ out.push({ el, opts: { ...opts, duration, delay, stagger: 0 }, end: delay + duration });
959
+ end = Math.max(end, delay + duration);
960
+ });
961
+ cursor = end;
962
+ });
963
+ return out;
964
+ }
965
+ /**
966
+ * Build a timeline of animations.
967
+ *
968
+ * @example
969
+ * sequence([
970
+ * { target: '.title', animation: 'fade-in-up' },
971
+ * { target: '.subtitle', animation: 'blur-in', gap: -300 }, // overlap by 300ms
972
+ * { target: '.card', animation: 'scale-up', stagger: 80 },
973
+ * ], { trigger: '.hero' });
974
+ */
975
+ function sequence(steps, options = {}) {
976
+ var _a, _b;
977
+ const { trigger, instance, ...defaults } = options;
978
+ const sa = () => instance || fallback || (fallback = createScrollAnimate());
979
+ let io;
980
+ let active = [];
981
+ let settle;
982
+ const controller = {
983
+ play() {
984
+ controller.cancel();
985
+ if (!hasDOM())
986
+ return Promise.resolve();
987
+ active = plan(steps, defaults);
988
+ return new Promise((resolve) => {
989
+ let left = active.length;
990
+ settle = () => {
991
+ settle = undefined;
992
+ resolve();
993
+ };
994
+ if (!left)
995
+ return settle();
996
+ const run = active;
997
+ run.forEach(({ el, opts }) => {
998
+ const done = opts.onComplete;
999
+ sa().animate(el, {
1000
+ ...opts,
1001
+ onComplete: (node) => {
1002
+ done === null || done === void 0 ? void 0 : done(node);
1003
+ if (run === active && --left === 0)
1004
+ settle === null || settle === void 0 ? void 0 : settle();
1005
+ },
1006
+ });
1007
+ });
1008
+ });
1009
+ },
1010
+ cancel() {
1011
+ io === null || io === void 0 ? void 0 : io.disconnect();
1012
+ io = undefined;
1013
+ active.forEach(({ el }) => stopAnimation(el));
1014
+ active = [];
1015
+ settle === null || settle === void 0 ? void 0 : settle();
1016
+ },
1017
+ duration() {
1018
+ return plan(steps, defaults).reduce((max, p) => Math.max(max, p.end), 0);
1019
+ },
1020
+ };
1021
+ if (trigger && hasDOM() && supportsObserver()) {
1022
+ const el = resolveTargets(trigger)[0];
1023
+ if (el) {
1024
+ plan(steps, defaults).forEach(({ el: target }) => prepareElement(target));
1025
+ io = new IntersectionObserver((entries) => {
1026
+ if (!entries.some((e) => e.isIntersecting))
1027
+ return;
1028
+ io === null || io === void 0 ? void 0 : io.disconnect();
1029
+ io = undefined;
1030
+ controller.play();
1031
+ }, { threshold: (_a = defaults.threshold) !== null && _a !== void 0 ? _a : 0.1, rootMargin: (_b = defaults.rootMargin) !== null && _b !== void 0 ? _b : '0px' });
1032
+ io.observe(el);
1033
+ }
1034
+ }
1035
+ return controller;
1036
+ }
1037
+
1038
+ /**
1039
+ * use-scroll-animate - React Integration
1040
+ * Provides useScrollAnimate and useScrollStagger hooks for React applications.
1041
+ * `useScrollStagger({ observeChildren: true })` also animates children added later.
1042
+ *
1043
+ * Both hooks are thin wrappers around the core engine, so they share its
1044
+ * behaviour: `once`, `offset`, custom easing functions, parallax,
1045
+ * `prefers-reduced-motion` support, and proper cleanup on unmount.
1046
+ */
1047
+ const CALLBACKS = ['onStart', 'onComplete', 'onEnter', 'onLeave', 'onProgress'];
1048
+ /**
1049
+ * Wrap the callbacks that exist at mount so they always call the latest
1050
+ * version from the most recent render (avoids stale closures without
1051
+ * re-creating observers on every render).
1052
+ * @internal
1053
+ */
1054
+ function withLatestCallbacks(latest) {
1055
+ const initial = latest.current || {};
1056
+ const opts = { ...initial };
1057
+ CALLBACKS.forEach((name) => {
1058
+ if (typeof initial[name] === 'function') {
1059
+ opts[name] = (...args) => { var _a, _b; return (_b = (_a = latest.current) === null || _a === void 0 ? void 0 : _a[name]) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); };
1060
+ }
1061
+ });
1062
+ return opts;
1063
+ }
1064
+ function createReactHooks(React) {
1065
+ // Created lazily on the client so importing on the server is side-effect free.
1066
+ let instance = null;
1067
+ const getInstance = () => instance || (instance = createScrollAnimate());
1068
+ function useScrollAnimate(options = {}) {
1069
+ const ref = React.useRef(null);
1070
+ const optionsRef = React.useRef(options);
1071
+ optionsRef.current = options;
1072
+ React.useEffect(() => {
1073
+ const el = ref.current;
1074
+ if (!el)
1075
+ return;
1076
+ const sa = getInstance();
1077
+ sa.observe(el, withLatestCallbacks(optionsRef));
1078
+ return () => sa.unobserve(el);
1079
+ }, []);
1080
+ return ref;
1081
+ }
1082
+ function useScrollStagger(options = {}) {
1083
+ const ref = React.useRef(null);
1084
+ const optionsRef = React.useRef(options);
1085
+ optionsRef.current = options;
1086
+ React.useEffect(() => {
1087
+ const container = ref.current;
1088
+ if (!container)
1089
+ return;
1090
+ return staggerChildren(container, withLatestCallbacks(optionsRef), getInstance());
1091
+ }, []);
1092
+ return ref;
1093
+ }
1094
+ return { useScrollAnimate, useScrollStagger };
1095
+ }
1096
+
1097
+ /**
1098
+ * use-scroll-animate - Vue 3 Integration
1099
+ * Provides useScrollAnimate and useScrollStagger composables for Vue 3 applications.
1100
+ *
1101
+ * A thin wrapper around the core engine, so it shares its behaviour: `once`,
1102
+ * `offset`, custom easing functions, parallax, `prefers-reduced-motion`
1103
+ * support, and cleanup on unmount.
1104
+ */
1105
+ /** Support refs on components (`$el`) as well as plain elements. */
1106
+ function unwrap(value) {
1107
+ if (value && typeof Element !== 'undefined' && !(value instanceof Element) && value.$el instanceof Element) {
1108
+ return value.$el;
1109
+ }
1110
+ return value || null;
1111
+ }
1112
+ function createVueComposables(Vue) {
1113
+ // Created lazily on the client so importing on the server is side-effect free.
1114
+ let instance = null;
1115
+ const getInstance = () => instance || (instance = createScrollAnimate());
1116
+ function useScrollAnimate(options = {}) {
1117
+ const animateRef = Vue.ref(null);
1118
+ let el = null;
1119
+ Vue.onMounted(() => {
1120
+ const target = unwrap(animateRef.value);
1121
+ if (!target)
1122
+ return;
1123
+ el = target;
1124
+ getInstance().observe(el, options);
1125
+ });
1126
+ Vue.onUnmounted(() => {
1127
+ if (el)
1128
+ getInstance().unobserve(el);
1129
+ el = null;
1130
+ });
1131
+ return { animateRef };
1132
+ }
1133
+ /** Stagger the children of `staggerRef`; `observeChildren: true` also animates children added later. */
1134
+ function useScrollStagger(options = {}) {
1135
+ const staggerRef = Vue.ref(null);
1136
+ let stop;
1137
+ Vue.onMounted(() => {
1138
+ const target = unwrap(staggerRef.value);
1139
+ if (target)
1140
+ stop = staggerChildren(target, options, getInstance());
1141
+ });
1142
+ Vue.onUnmounted(() => {
1143
+ stop === null || stop === void 0 ? void 0 : stop();
1144
+ stop = undefined;
1145
+ });
1146
+ return { staggerRef };
1147
+ }
1148
+ return { useScrollAnimate, useScrollStagger };
1149
+ }
1150
+
1151
+ /**
1152
+ * use-scroll-animate
1153
+ *
1154
+ * A lightweight, dependency-free scroll animation library for modern web
1155
+ * applications. Built with TypeScript, powered by IntersectionObserver and
1156
+ * the Web Animations API. Safe to import during SSR.
1157
+ *
1158
+ * @license MIT
1159
+ * @see https://github.com/HarrisonCN/use-scroll-animate
1160
+ */
1161
+ /**
1162
+ * Default singleton instance of ScrollAnimate.
1163
+ * Ready to use out of the box with sensible defaults.
1164
+ *
1165
+ * @example
1166
+ * ```js
1167
+ * import ScrollAnimate from 'use-scroll-animate';
1168
+ *
1169
+ * // Auto-initialize all elements with data-sa attribute
1170
+ * ScrollAnimate.init();
1171
+ *
1172
+ * // Or manually observe elements
1173
+ * ScrollAnimate.observe('.my-element', { animation: 'fade-in-up' });
1174
+ * ```
1175
+ */
1176
+ const ScrollAnimate = createScrollAnimate();
1177
+
1178
+ export { EASING_MAP, PRESETS, createReactHooks, createScrollAnimate, createVueComposables, ScrollAnimate as default, getScrollProgress, resolveEasing, resolvePreset, sequence, staggerChildren };
1179
+ //# sourceMappingURL=index.esm.js.map