react-scroll-media 1.0.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.mjs ADDED
@@ -0,0 +1,799 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/react/ScrollSequence.tsx
6
+ import React2, { useRef as useRef3 } from "react";
7
+
8
+ // src/react/useScrollSequence.ts
9
+ import { useRef, useEffect, useState } from "react";
10
+
11
+ // src/controllers/imageController.ts
12
+ var ImageController = class {
13
+ constructor(config) {
14
+ __publicField(this, "canvas");
15
+ __publicField(this, "ctx");
16
+ __publicField(this, "frames");
17
+ __publicField(this, "imageCache", /* @__PURE__ */ new Map());
18
+ __publicField(this, "loadingPromises", /* @__PURE__ */ new Map());
19
+ __publicField(this, "currentFrameIndex", -1);
20
+ __publicField(this, "strategy");
21
+ __publicField(this, "bufferSize");
22
+ /**
23
+ * Create a new ImageController instance.
24
+ *
25
+ * @param config - Configuration object
26
+ * @throws If canvas doesn't support 2D context
27
+ */
28
+ __publicField(this, "isDestroyed", false);
29
+ this.canvas = config.canvas;
30
+ this.frames = config.frames;
31
+ this.strategy = config.strategy || "eager";
32
+ this.bufferSize = config.bufferSize || 10;
33
+ const ctx = this.canvas.getContext("2d");
34
+ if (!ctx) {
35
+ throw new Error("Failed to get 2D context from canvas");
36
+ }
37
+ this.ctx = ctx;
38
+ if (this.strategy === "eager") {
39
+ this.preloadAll();
40
+ } else {
41
+ this.ensureFrameWindow(0);
42
+ }
43
+ }
44
+ // ... preloadAll omitted for brevity if unchanged, but let's include for completeness if needed.
45
+ // Actually, we need to add guards to preloadFrame, so let's check it.
46
+ preloadAll() {
47
+ this.frames.forEach((_, index) => this.preloadFrame(index));
48
+ }
49
+ ensureFrameWindow(currentIndex) {
50
+ if (this.isDestroyed) return;
51
+ const radius = this.bufferSize;
52
+ const start = Math.max(0, currentIndex - radius);
53
+ const end = Math.min(this.frames.length - 1, currentIndex + radius);
54
+ const needed = /* @__PURE__ */ new Set();
55
+ for (let i = start; i <= end; i++) {
56
+ needed.add(this.frames[i]);
57
+ }
58
+ for (const [src] of this.imageCache) {
59
+ if (!needed.has(src)) {
60
+ this.imageCache.delete(src);
61
+ this.loadingPromises.delete(src);
62
+ }
63
+ }
64
+ for (let i = start; i <= end; i++) {
65
+ void this.preloadFrame(i);
66
+ }
67
+ }
68
+ async preloadFrame(index) {
69
+ if (this.isDestroyed || index < 0 || index >= this.frames.length) return;
70
+ const src = this.frames[index];
71
+ if (this.imageCache.has(src)) return;
72
+ if (!this.loadingPromises.has(src)) {
73
+ this.loadingPromises.set(src, this.loadImage(src));
74
+ }
75
+ try {
76
+ await this.loadingPromises.get(src);
77
+ } catch {
78
+ if (!this.isDestroyed) {
79
+ }
80
+ }
81
+ }
82
+ loadImage(src) {
83
+ return new Promise((resolve, reject) => {
84
+ const img = new Image();
85
+ img.onload = () => {
86
+ if (this.isDestroyed) return;
87
+ img.decode().then(() => {
88
+ if (this.isDestroyed) return;
89
+ this.imageCache.set(src, img);
90
+ resolve(img);
91
+ }).catch(() => {
92
+ if (this.isDestroyed) return;
93
+ this.imageCache.set(src, img);
94
+ resolve(img);
95
+ });
96
+ };
97
+ img.onerror = () => {
98
+ if (this.isDestroyed) return;
99
+ reject(new Error(`Failed to load image: ${src}`));
100
+ };
101
+ img.src = src;
102
+ });
103
+ }
104
+ update(progress) {
105
+ if (this.isDestroyed || this.frames.length === 0) return;
106
+ const frameIndex = Math.floor(progress * (this.frames.length - 1));
107
+ if (this.strategy === "lazy") {
108
+ this.ensureFrameWindow(frameIndex);
109
+ }
110
+ if (frameIndex === this.currentFrameIndex) return;
111
+ this.currentFrameIndex = frameIndex;
112
+ this.drawFrame(frameIndex);
113
+ }
114
+ drawFrame(index) {
115
+ if (this.isDestroyed || index < 0 || index >= this.frames.length) return;
116
+ const src = this.frames[index];
117
+ const img = this.imageCache.get(src);
118
+ if (!img) {
119
+ const promise = this.loadingPromises.get(src);
120
+ if (promise) {
121
+ promise.then(() => {
122
+ if (this.currentFrameIndex === index) {
123
+ this.drawFrame(index);
124
+ }
125
+ }).catch(() => {
126
+ });
127
+ }
128
+ return;
129
+ }
130
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
131
+ const scale = Math.min(
132
+ this.canvas.width / img.width,
133
+ this.canvas.height / img.height
134
+ );
135
+ const scaledWidth = img.width * scale;
136
+ const scaledHeight = img.height * scale;
137
+ const x = (this.canvas.width - scaledWidth) / 2;
138
+ const y = (this.canvas.height - scaledHeight) / 2;
139
+ this.ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
140
+ }
141
+ setCanvasSize(width, height) {
142
+ if (this.isDestroyed) return;
143
+ this.canvas.width = width;
144
+ this.canvas.height = height;
145
+ if (this.currentFrameIndex >= 0) {
146
+ this.drawFrame(this.currentFrameIndex);
147
+ }
148
+ }
149
+ destroy() {
150
+ this.isDestroyed = true;
151
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
152
+ this.imageCache.clear();
153
+ this.loadingPromises.clear();
154
+ }
155
+ };
156
+
157
+ // src/sequence/sequenceResolver.ts
158
+ async function resolveSequence(source) {
159
+ switch (source.type) {
160
+ case "manual":
161
+ return processManualFrames(source.frames);
162
+ case "pattern":
163
+ return processPatternMode(source.url, source.start ?? 1, source.end, source.pad);
164
+ case "manifest":
165
+ return processManifestMode(source.url);
166
+ default:
167
+ return { frames: [], frameCount: 0 };
168
+ }
169
+ }
170
+ function processManualFrames(frames) {
171
+ const sorted = [...frames].sort((a, b) => {
172
+ const numA = extractNumber(a);
173
+ const numB = extractNumber(b);
174
+ return numA - numB;
175
+ });
176
+ return {
177
+ frames: sorted,
178
+ frameCount: sorted.length
179
+ };
180
+ }
181
+ function processPatternMode(pattern, start, end, pad) {
182
+ const frames = [];
183
+ for (let i = start; i <= end; i++) {
184
+ let indexStr = i.toString();
185
+ if (pad) {
186
+ indexStr = indexStr.padStart(pad, "0");
187
+ }
188
+ frames.push(pattern.replace("{index}", indexStr));
189
+ }
190
+ return {
191
+ frames,
192
+ frameCount: frames.length
193
+ };
194
+ }
195
+ var manifestCache = /* @__PURE__ */ new Map();
196
+ async function processManifestMode(url) {
197
+ if (manifestCache.has(url)) {
198
+ return manifestCache.get(url);
199
+ }
200
+ const promise = (async () => {
201
+ try {
202
+ const res = await fetch(url);
203
+ if (!res.ok) {
204
+ throw new Error(`Failed to fetch manifest: ${res.statusText}`);
205
+ }
206
+ const data = await res.json();
207
+ if (data.frames && Array.isArray(data.frames)) {
208
+ return processManualFrames(data.frames);
209
+ }
210
+ if (data.pattern && typeof data.end === "number") {
211
+ const start = data.start ?? 1;
212
+ const pad = data.pad;
213
+ return processPatternMode(data.pattern, start, data.end, pad);
214
+ }
215
+ return { frames: [], frameCount: 0 };
216
+ } catch (err) {
217
+ manifestCache.delete(url);
218
+ throw err;
219
+ }
220
+ })();
221
+ manifestCache.set(url, promise);
222
+ return promise;
223
+ }
224
+ function extractNumber(filename) {
225
+ const match = filename.match(/\d+/);
226
+ return match ? parseInt(match[0], 10) : -1;
227
+ }
228
+
229
+ // src/core/clamp.ts
230
+ function clamp(value, min = 0, max = 1) {
231
+ return Math.max(min, Math.min(max, value));
232
+ }
233
+
234
+ // src/react/scrollTimelineContext.ts
235
+ import { createContext, useContext } from "react";
236
+ var ScrollTimelineContext = createContext({
237
+ timeline: null
238
+ });
239
+ function useTimelineContext() {
240
+ return useContext(ScrollTimelineContext);
241
+ }
242
+
243
+ // src/react/useScrollTimeline.ts
244
+ function useScrollTimeline() {
245
+ const { timeline } = useTimelineContext();
246
+ const subscribe = (callback) => {
247
+ if (!timeline) return () => {
248
+ };
249
+ return timeline.subscribe(callback);
250
+ };
251
+ return { subscribe, timeline };
252
+ }
253
+
254
+ // src/react/useScrollSequence.ts
255
+ function useScrollSequence({
256
+ source,
257
+ debugRef,
258
+ memoryStrategy = "eager",
259
+ lazyBuffer = 10,
260
+ onError
261
+ }) {
262
+ const canvasRef = useRef(null);
263
+ const controllerRef = useRef(null);
264
+ const { subscribe } = useScrollTimeline();
265
+ const [isLoaded, setIsLoaded] = useState(false);
266
+ const [error, setError] = useState(null);
267
+ useEffect(() => {
268
+ let active = true;
269
+ let currentController = null;
270
+ let unsubscribeTimeline = null;
271
+ const init = async () => {
272
+ setIsLoaded(false);
273
+ setError(null);
274
+ const canvas = canvasRef.current;
275
+ if (!canvas) return;
276
+ try {
277
+ if (typeof window === "undefined") return;
278
+ const sequence = await resolveSequence(source);
279
+ if (!active) return;
280
+ if (sequence.frames.length === 0) {
281
+ return;
282
+ }
283
+ if (typeof window !== "undefined") {
284
+ canvas.width = window.innerWidth;
285
+ canvas.height = window.innerHeight;
286
+ }
287
+ currentController = new ImageController({
288
+ canvas,
289
+ frames: sequence.frames,
290
+ strategy: memoryStrategy,
291
+ bufferSize: lazyBuffer
292
+ });
293
+ controllerRef.current = currentController;
294
+ unsubscribeTimeline = subscribe((progress) => {
295
+ if (!currentController) return;
296
+ const clamped = clamp(progress);
297
+ currentController.update(clamped);
298
+ if (debugRef?.current) {
299
+ const frameIndex = Math.floor(clamped * (sequence.frames.length - 1));
300
+ debugRef.current.innerText = `Progress: ${clamped.toFixed(2)}
301
+ Frame: ${frameIndex + 1} / ${sequence.frames.length}`;
302
+ }
303
+ });
304
+ if (active) setIsLoaded(true);
305
+ } catch (err) {
306
+ if (active) {
307
+ const e = err instanceof Error ? err : new Error("Unknown initialization error");
308
+ setError(e);
309
+ if (onError) onError(e);
310
+ }
311
+ }
312
+ };
313
+ init();
314
+ return () => {
315
+ active = false;
316
+ currentController?.destroy();
317
+ controllerRef.current = null;
318
+ if (unsubscribeTimeline) unsubscribeTimeline();
319
+ };
320
+ }, [source, memoryStrategy, lazyBuffer, subscribe]);
321
+ return {
322
+ canvasRef,
323
+ isLoaded,
324
+ error
325
+ };
326
+ }
327
+
328
+ // src/react/ScrollTimelineProvider.tsx
329
+ import React, { useRef as useRef2, useState as useState2 } from "react";
330
+
331
+ // src/core/loopManager.ts
332
+ var _ScrollLoopManager = class _ScrollLoopManager {
333
+ constructor() {
334
+ __publicField(this, "callbacks", /* @__PURE__ */ new Set());
335
+ __publicField(this, "rafId", null);
336
+ __publicField(this, "isActive", false);
337
+ __publicField(this, "tick", () => {
338
+ if (!this.isActive) return;
339
+ this.callbacks.forEach((cb) => {
340
+ try {
341
+ cb();
342
+ } catch (e) {
343
+ }
344
+ });
345
+ this.rafId = requestAnimationFrame(this.tick);
346
+ });
347
+ }
348
+ static getInstance() {
349
+ if (!_ScrollLoopManager.instance) {
350
+ _ScrollLoopManager.instance = new _ScrollLoopManager();
351
+ }
352
+ return _ScrollLoopManager.instance;
353
+ }
354
+ /**
355
+ * Register a callback to be called on every animation frame.
356
+ */
357
+ register(callback) {
358
+ if (this.callbacks.has(callback)) return;
359
+ this.callbacks.add(callback);
360
+ if (this.callbacks.size === 1) {
361
+ this.start();
362
+ }
363
+ }
364
+ /**
365
+ * Unregister a callback.
366
+ */
367
+ unregister(callback) {
368
+ this.callbacks.delete(callback);
369
+ if (this.callbacks.size === 0) {
370
+ this.stop();
371
+ }
372
+ }
373
+ start() {
374
+ if (this.isActive) return;
375
+ this.isActive = true;
376
+ if (typeof window !== "undefined") {
377
+ this.tick();
378
+ }
379
+ }
380
+ stop() {
381
+ this.isActive = false;
382
+ if (this.rafId !== null && typeof window !== "undefined") {
383
+ cancelAnimationFrame(this.rafId);
384
+ this.rafId = null;
385
+ }
386
+ }
387
+ };
388
+ __publicField(_ScrollLoopManager, "instance");
389
+ var ScrollLoopManager = _ScrollLoopManager;
390
+
391
+ // src/constants.ts
392
+ var SCROLL_THRESHOLD = 1e-4;
393
+
394
+ // src/core/scrollTimeline.ts
395
+ var ScrollTimeline = class {
396
+ constructor(container) {
397
+ __publicField(this, "container");
398
+ __publicField(this, "subscribers", /* @__PURE__ */ new Set());
399
+ __publicField(this, "currentProgress", 0);
400
+ // Caching for performance
401
+ __publicField(this, "cachedRect", null);
402
+ __publicField(this, "cachedScrollParent", null);
403
+ __publicField(this, "cachedScrollParentRect", null);
404
+ __publicField(this, "cachedViewportHeight", 0);
405
+ __publicField(this, "cachedOffsetTop", 0);
406
+ __publicField(this, "isLayoutDirty", true);
407
+ __publicField(this, "resizeObserver", null);
408
+ __publicField(this, "id", typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 9));
409
+ __publicField(this, "onWindowResize", () => {
410
+ this.isLayoutDirty = true;
411
+ });
412
+ __publicField(this, "tick", () => {
413
+ const progress = this.calculateProgress();
414
+ if (Math.abs(progress - this.currentProgress) > SCROLL_THRESHOLD) {
415
+ this.currentProgress = progress;
416
+ this.notify();
417
+ }
418
+ });
419
+ this.container = container;
420
+ if (typeof window !== "undefined") {
421
+ this.resizeObserver = new ResizeObserver(() => {
422
+ this.isLayoutDirty = true;
423
+ });
424
+ this.resizeObserver.observe(this.container);
425
+ if (document.body) {
426
+ this.resizeObserver.observe(document.body);
427
+ }
428
+ window.addEventListener("resize", this.onWindowResize);
429
+ }
430
+ }
431
+ /**
432
+ * Subscribe to progress updates.
433
+ * Returns an unsubscribe function.
434
+ */
435
+ subscribe(callback) {
436
+ this.subscribers.add(callback);
437
+ try {
438
+ callback(this.currentProgress);
439
+ } catch (e) {
440
+ }
441
+ if (this.subscribers.size === 1) {
442
+ ScrollLoopManager.getInstance().register(this.tick);
443
+ }
444
+ return () => {
445
+ this.subscribers.delete(callback);
446
+ if (this.subscribers.size === 0) {
447
+ ScrollLoopManager.getInstance().unregister(this.tick);
448
+ }
449
+ };
450
+ }
451
+ unsubscribe(callback) {
452
+ this.subscribers.delete(callback);
453
+ if (this.subscribers.size === 0) {
454
+ ScrollLoopManager.getInstance().unregister(this.tick);
455
+ }
456
+ }
457
+ /**
458
+ * Start is now handled by LoopManager via subscriptions
459
+ * Deprecated but kept for API stability if needed.
460
+ */
461
+ start() {
462
+ }
463
+ stop() {
464
+ ScrollLoopManager.getInstance().unregister(this.tick);
465
+ }
466
+ notify() {
467
+ this.subscribers.forEach((cb) => {
468
+ try {
469
+ cb(this.currentProgress);
470
+ } catch (e) {
471
+ }
472
+ });
473
+ }
474
+ updateCache() {
475
+ if (!this.isLayoutDirty && this.cachedRect) return;
476
+ this.cachedRect = this.container.getBoundingClientRect();
477
+ if (!this.cachedScrollParent) {
478
+ this.cachedScrollParent = this.getScrollParent(this.container);
479
+ }
480
+ if (this.cachedScrollParent instanceof Element) {
481
+ this.cachedScrollParentRect = this.cachedScrollParent.getBoundingClientRect();
482
+ this.cachedViewportHeight = this.cachedScrollParentRect.height;
483
+ this.cachedOffsetTop = this.cachedScrollParentRect.top;
484
+ } else if (typeof window !== "undefined") {
485
+ this.cachedViewportHeight = window.innerHeight;
486
+ this.cachedOffsetTop = 0;
487
+ }
488
+ this.isLayoutDirty = false;
489
+ }
490
+ calculateProgress() {
491
+ if (this.isLayoutDirty || !this.cachedRect) {
492
+ this.updateCache();
493
+ }
494
+ const currentRect = this.container.getBoundingClientRect();
495
+ const scrollDist = (this.cachedRect?.height || currentRect.height) - this.cachedViewportHeight;
496
+ if (scrollDist <= 0) return 1;
497
+ const relativeTop = currentRect.top - this.cachedOffsetTop;
498
+ const rawProgress = -relativeTop / scrollDist;
499
+ const clamped = Math.min(Math.max(rawProgress, 0), 1);
500
+ return Math.round(clamped * 1e6) / 1e6;
501
+ }
502
+ getScrollParent(node) {
503
+ if (typeof window === "undefined") return node;
504
+ let current = node.parentElement;
505
+ while (current) {
506
+ const style = getComputedStyle(current);
507
+ if (["auto", "scroll"].includes(style.overflowY)) {
508
+ return current;
509
+ }
510
+ current = current.parentElement;
511
+ }
512
+ return window;
513
+ }
514
+ destroy() {
515
+ this.subscribers.clear();
516
+ ScrollLoopManager.getInstance().unregister(this.tick);
517
+ if (this.resizeObserver) {
518
+ this.resizeObserver.disconnect();
519
+ }
520
+ if (typeof window !== "undefined") {
521
+ window.removeEventListener("resize", this.onWindowResize);
522
+ }
523
+ }
524
+ };
525
+
526
+ // src/react/ScrollTimelineProvider.tsx
527
+ import { jsx } from "react/jsx-runtime";
528
+ function ScrollTimelineProvider({
529
+ children,
530
+ scrollLength = "300vh",
531
+ className = "",
532
+ style = {}
533
+ }) {
534
+ const containerRef = useRef2(null);
535
+ const [timeline, setTimeline] = useState2(null);
536
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
537
+ useIsomorphicLayoutEffect(() => {
538
+ if (typeof window === "undefined") return;
539
+ if (!containerRef.current) return;
540
+ const instance = new ScrollTimeline(containerRef.current);
541
+ setTimeline(instance);
542
+ return () => {
543
+ instance.destroy();
544
+ setTimeline(null);
545
+ };
546
+ }, []);
547
+ const containerStyle = {
548
+ height: scrollLength,
549
+ position: "relative",
550
+ width: "100%",
551
+ ...style
552
+ };
553
+ const stickyWrapperStyle = {
554
+ position: "sticky",
555
+ top: 0,
556
+ height: "100vh",
557
+ width: "100%",
558
+ overflow: "hidden"
559
+ };
560
+ const contextValue = React.useMemo(() => ({ timeline }), [timeline]);
561
+ return /* @__PURE__ */ jsx(ScrollTimelineContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
562
+ "div",
563
+ {
564
+ ref: containerRef,
565
+ className,
566
+ style: containerStyle,
567
+ children: /* @__PURE__ */ jsx("div", { style: stickyWrapperStyle, children })
568
+ }
569
+ ) });
570
+ }
571
+
572
+ // src/react/ScrollSequence.tsx
573
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
574
+ var InnerSequence = ({
575
+ source,
576
+ debug,
577
+ memoryStrategy,
578
+ lazyBuffer,
579
+ accessibilityLabel = "Scroll sequence",
580
+ fallback,
581
+ onError
582
+ }) => {
583
+ const debugRef = useRef3(null);
584
+ const { canvasRef, isLoaded } = useScrollSequence({
585
+ source,
586
+ debugRef,
587
+ memoryStrategy,
588
+ lazyBuffer,
589
+ onError
590
+ });
591
+ const canvasStyle = {
592
+ display: "block",
593
+ width: "100%",
594
+ height: "100%",
595
+ objectFit: "cover",
596
+ opacity: isLoaded ? 1 : 0,
597
+ transition: "opacity 0.2s ease-in"
598
+ };
599
+ const debugStyle = {
600
+ position: "absolute",
601
+ top: "10px",
602
+ left: "10px",
603
+ background: "rgba(0, 0, 0, 0.7)",
604
+ color: "#00ff00",
605
+ padding: "8px",
606
+ borderRadius: "4px",
607
+ fontFamily: "monospace",
608
+ fontSize: "12px",
609
+ pointerEvents: "none",
610
+ whiteSpace: "pre-wrap",
611
+ zIndex: 9999
612
+ };
613
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
614
+ !isLoaded && fallback && /* @__PURE__ */ jsx2("div", { style: { position: "absolute", inset: 0, zIndex: 1 }, children: fallback }),
615
+ /* @__PURE__ */ jsx2(
616
+ "canvas",
617
+ {
618
+ ref: canvasRef,
619
+ style: canvasStyle,
620
+ role: "img",
621
+ "aria-label": accessibilityLabel
622
+ }
623
+ ),
624
+ debug && /* @__PURE__ */ jsx2("div", { ref: debugRef, style: debugStyle, children: "Waiting for scroll..." })
625
+ ] });
626
+ };
627
+ var ScrollSequence = React2.forwardRef(
628
+ (props, ref) => {
629
+ const {
630
+ source,
631
+ scrollLength = "300vh",
632
+ className = "",
633
+ debug = false,
634
+ memoryStrategy = "eager",
635
+ lazyBuffer = 10,
636
+ fallback,
637
+ accessibilityLabel,
638
+ onError
639
+ } = props;
640
+ const prefersReducedMotion = React2.useMemo(() => {
641
+ if (typeof window !== "undefined") {
642
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
643
+ }
644
+ return false;
645
+ }, []);
646
+ return /* @__PURE__ */ jsx2("div", { ref, className, style: { width: "100%" }, children: /* @__PURE__ */ jsxs(ScrollTimelineProvider, { scrollLength, children: [
647
+ prefersReducedMotion && fallback ? /* @__PURE__ */ jsx2("div", { style: { position: "sticky", top: 0, height: "100vh", width: "100%" }, children: fallback }) : /* @__PURE__ */ jsx2(
648
+ InnerSequence,
649
+ {
650
+ source,
651
+ debug,
652
+ memoryStrategy,
653
+ lazyBuffer,
654
+ fallback,
655
+ accessibilityLabel,
656
+ onError
657
+ }
658
+ ),
659
+ props.children
660
+ ] }) });
661
+ }
662
+ );
663
+
664
+ // src/react/ScrollText.tsx
665
+ import { useRef as useRef4, useEffect as useEffect2 } from "react";
666
+ import { jsx as jsx3 } from "react/jsx-runtime";
667
+ function ScrollText({
668
+ children,
669
+ start = 0,
670
+ end = 0.2,
671
+ exitStart,
672
+ exitEnd,
673
+ initialOpacity = 0,
674
+ targetOpacity = 1,
675
+ finalOpacity = 0,
676
+ translateY = 50,
677
+ style,
678
+ className
679
+ }) {
680
+ const ref = useRef4(null);
681
+ const { subscribe } = useScrollTimeline();
682
+ useEffect2(() => {
683
+ if (typeof window === "undefined") return;
684
+ const unsubscribe = subscribe((progress) => {
685
+ if (!ref.current) return;
686
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
687
+ const effectiveTranslateY = prefersReducedMotion ? 0 : translateY;
688
+ let opacity = initialOpacity;
689
+ let currentY = effectiveTranslateY;
690
+ if (progress < start) {
691
+ opacity = initialOpacity;
692
+ currentY = effectiveTranslateY;
693
+ } else if (progress >= start && progress <= end) {
694
+ const local = (progress - start) / (end - start);
695
+ opacity = initialOpacity + (targetOpacity - initialOpacity) * local;
696
+ currentY = effectiveTranslateY * (1 - local);
697
+ } else if (!exitStart || progress < exitStart) {
698
+ opacity = targetOpacity;
699
+ currentY = 0;
700
+ } else if (exitStart && exitEnd && progress >= exitStart && progress <= exitEnd) {
701
+ const local = (progress - exitStart) / (exitEnd - exitStart);
702
+ opacity = targetOpacity + (finalOpacity - targetOpacity) * local;
703
+ currentY = -effectiveTranslateY * local;
704
+ } else {
705
+ opacity = finalOpacity;
706
+ currentY = -effectiveTranslateY;
707
+ }
708
+ ref.current.style.opacity = opacity.toFixed(3);
709
+ ref.current.style.transform = `translateY(${currentY}px)`;
710
+ });
711
+ return unsubscribe;
712
+ }, [subscribe, start, end, exitStart, exitEnd, initialOpacity, targetOpacity, finalOpacity, translateY]);
713
+ return /* @__PURE__ */ jsx3(
714
+ "div",
715
+ {
716
+ ref,
717
+ className,
718
+ style: {
719
+ opacity: initialOpacity,
720
+ transform: `translateY(${translateY}px)`,
721
+ transition: "none",
722
+ // Critical: no CSS transition fighting JS
723
+ willChange: "opacity, transform",
724
+ ...style
725
+ },
726
+ children
727
+ }
728
+ );
729
+ }
730
+
731
+ // src/react/ScrollWordReveal.tsx
732
+ import { useRef as useRef5, useEffect as useEffect3 } from "react";
733
+ import { jsx as jsx4 } from "react/jsx-runtime";
734
+ function ScrollWordReveal({
735
+ text,
736
+ start = 0,
737
+ end = 1,
738
+ className,
739
+ style
740
+ }) {
741
+ const containerRef = useRef5(null);
742
+ const { subscribe } = useScrollTimeline();
743
+ const words = text.split(/\s+/);
744
+ useEffect3(() => {
745
+ const unsubscribe = subscribe((globalProgress) => {
746
+ if (!containerRef.current) return;
747
+ const spans = containerRef.current.children;
748
+ let localProgress = 0;
749
+ if (globalProgress <= start) localProgress = 0;
750
+ else if (globalProgress >= end) localProgress = 1;
751
+ else localProgress = (globalProgress - start) / (end - start);
752
+ const totalWords = spans.length;
753
+ const progressPerWord = 1 / totalWords;
754
+ for (let i = 0; i < totalWords; i++) {
755
+ const span = spans[i];
756
+ const wordStart = i * progressPerWord;
757
+ const wordEnd = (i + 1) * progressPerWord;
758
+ let wordOpacity = 0;
759
+ if (localProgress >= wordEnd) {
760
+ wordOpacity = 1;
761
+ } else if (localProgress <= wordStart) {
762
+ wordOpacity = 0.1;
763
+ } else {
764
+ wordOpacity = 0.1 + 0.9 * ((localProgress - wordStart) / (wordEnd - wordStart));
765
+ }
766
+ span.style.opacity = wordOpacity.toFixed(2);
767
+ const translate = (1 - wordOpacity) * 10;
768
+ span.style.transform = `translateY(${translate}px)`;
769
+ }
770
+ });
771
+ return unsubscribe;
772
+ }, [subscribe, start, end]);
773
+ return /* @__PURE__ */ jsx4("div", { ref: containerRef, className, style: { ...style, display: "flex", flexWrap: "wrap", gap: "0.25em" }, children: words.map((word, i) => /* @__PURE__ */ jsx4(
774
+ "span",
775
+ {
776
+ style: {
777
+ opacity: 0.1,
778
+ transform: "translateY(10px)",
779
+ transition: "none",
780
+ willChange: "opacity, transform"
781
+ },
782
+ children: word
783
+ },
784
+ i
785
+ )) });
786
+ }
787
+ export {
788
+ ImageController,
789
+ ScrollSequence,
790
+ ScrollText,
791
+ ScrollTimeline,
792
+ ScrollTimelineProvider,
793
+ ScrollWordReveal,
794
+ clamp,
795
+ resolveSequence,
796
+ useScrollSequence,
797
+ useScrollTimeline
798
+ };
799
+ //# sourceMappingURL=index.mjs.map