scroll-system 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +138 -3
  2. package/dist/components/AriaLiveRegion.d.ts +18 -0
  3. package/dist/components/AriaLiveRegion.d.ts.map +1 -0
  4. package/dist/components/ControlledView.d.ts +16 -0
  5. package/dist/components/ControlledView.d.ts.map +1 -0
  6. package/dist/components/FullView.d.ts +20 -0
  7. package/dist/components/FullView.d.ts.map +1 -0
  8. package/dist/components/LazyView.d.ts +33 -0
  9. package/dist/components/LazyView.d.ts.map +1 -0
  10. package/dist/components/NestedScrollView.d.ts +38 -0
  11. package/dist/components/NestedScrollView.d.ts.map +1 -0
  12. package/dist/components/ScrollContainer.d.ts +9 -0
  13. package/dist/components/ScrollContainer.d.ts.map +1 -0
  14. package/dist/components/ScrollDebugOverlay.d.ts +17 -0
  15. package/dist/components/ScrollDebugOverlay.d.ts.map +1 -0
  16. package/dist/components/ScrollLockedView.d.ts +10 -0
  17. package/dist/components/ScrollLockedView.d.ts.map +1 -0
  18. package/dist/components/index.d.ts +9 -0
  19. package/dist/components/index.d.ts.map +1 -0
  20. package/dist/constants.d.ts +17 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/hooks/index.d.ts +21 -0
  23. package/dist/hooks/index.d.ts.map +1 -0
  24. package/dist/hooks/useAutoScroll.d.ts +41 -0
  25. package/dist/hooks/useAutoScroll.d.ts.map +1 -0
  26. package/dist/hooks/useDragHandler.d.ts +28 -0
  27. package/dist/hooks/useDragHandler.d.ts.map +1 -0
  28. package/dist/hooks/useFocusManagement.d.ts +19 -0
  29. package/dist/hooks/useFocusManagement.d.ts.map +1 -0
  30. package/dist/hooks/useGestureConfig.d.ts +37 -0
  31. package/dist/hooks/useGestureConfig.d.ts.map +1 -0
  32. package/dist/hooks/useGlobalProgress.d.ts +36 -0
  33. package/dist/hooks/useGlobalProgress.d.ts.map +1 -0
  34. package/dist/hooks/useHashSync.d.ts +24 -0
  35. package/dist/hooks/useHashSync.d.ts.map +1 -0
  36. package/dist/hooks/useInfiniteScroll.d.ts +41 -0
  37. package/dist/hooks/useInfiniteScroll.d.ts.map +1 -0
  38. package/dist/hooks/useKeyboardHandler.d.ts +20 -0
  39. package/dist/hooks/useKeyboardHandler.d.ts.map +1 -0
  40. package/dist/hooks/useMetricsReporter.d.ts +23 -0
  41. package/dist/hooks/useMetricsReporter.d.ts.map +1 -0
  42. package/dist/hooks/useNavigation.d.ts +42 -0
  43. package/dist/hooks/useNavigation.d.ts.map +1 -0
  44. package/dist/hooks/useParallax.d.ts +33 -0
  45. package/dist/hooks/useParallax.d.ts.map +1 -0
  46. package/dist/hooks/usePreload.d.ts +31 -0
  47. package/dist/hooks/usePreload.d.ts.map +1 -0
  48. package/dist/hooks/useScrollAnalytics.d.ts +40 -0
  49. package/dist/hooks/useScrollAnalytics.d.ts.map +1 -0
  50. package/dist/hooks/useScrollLock.d.ts +40 -0
  51. package/dist/hooks/useScrollLock.d.ts.map +1 -0
  52. package/dist/hooks/useScrollSystem.d.ts +16 -0
  53. package/dist/hooks/useScrollSystem.d.ts.map +1 -0
  54. package/dist/hooks/useSnapPoints.d.ts +77 -0
  55. package/dist/hooks/useSnapPoints.d.ts.map +1 -0
  56. package/dist/hooks/useTouchHandler.d.ts +12 -0
  57. package/dist/hooks/useTouchHandler.d.ts.map +1 -0
  58. package/dist/hooks/useViewProgress.d.ts +22 -0
  59. package/dist/hooks/useViewProgress.d.ts.map +1 -0
  60. package/dist/hooks/useViewRegistration.d.ts +25 -0
  61. package/dist/hooks/useViewRegistration.d.ts.map +1 -0
  62. package/dist/hooks/useWheelHandler.d.ts +8 -0
  63. package/dist/hooks/useWheelHandler.d.ts.map +1 -0
  64. package/dist/index.d.ts +16 -2009
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +860 -284
  67. package/dist/index.mjs +799 -212
  68. package/dist/store/index.d.ts +2 -0
  69. package/dist/store/index.d.ts.map +1 -0
  70. package/dist/store/navigation.store.d.ts +23 -0
  71. package/dist/store/navigation.store.d.ts.map +1 -0
  72. package/dist/types/index.d.ts +315 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/dist/utils/index.d.ts +21 -0
  75. package/dist/utils/index.d.ts.map +1 -0
  76. package/dist/utils/normalizeWheel.d.ts +10 -0
  77. package/dist/utils/normalizeWheel.d.ts.map +1 -0
  78. package/package.json +4 -2
  79. package/dist/index.d.mts +0 -2010
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- // components/ScrollContainer.tsx
2
- import { useEffect as useEffect7, useRef as useRef6, useMemo as useMemo2, useState as useState2 } from "react";
1
+ import { createContext, useRef, useEffect, useState, useCallback, useMemo, useContext } from 'react';
2
+ import { create } from 'zustand';
3
+ import { subscribeWithSelector } from 'zustand/middleware';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
5
 
4
- // store/navigation.store.ts
5
- import { create } from "zustand";
6
- import { subscribeWithSelector } from "zustand/middleware";
6
+ // components/ScrollContainer.tsx
7
7
 
8
8
  // constants.ts
9
9
  var DEFAULT_TRANSITION_DURATION = 700;
@@ -22,6 +22,7 @@ function evaluateStateMachine(capability, progress, viewType, explicitLock) {
22
22
  if (explicitLock) return explicitLock;
23
23
  if (capability === "none") return "unlocked";
24
24
  if (viewType === "full") return "unlocked";
25
+ if (viewType === "nested") return "unlocked";
25
26
  if (progress >= 0.99) return "unlocked";
26
27
  return "locked";
27
28
  }
@@ -45,7 +46,12 @@ var initialState = {
45
46
  isTransitioning: false,
46
47
  isGlobalLocked: false,
47
48
  isDragging: false,
48
- globalProgress: 0
49
+ globalProgress: 0,
50
+ // NEW: AutoScroll state
51
+ isAutoScrolling: false,
52
+ isAutoScrollPaused: false,
53
+ // NEW: Infinite scroll
54
+ infiniteScrollEnabled: false
49
55
  };
50
56
  var lastNavigationTime = 0;
51
57
  var useScrollStore = create()(
@@ -70,12 +76,15 @@ var useScrollStore = create()(
70
76
  index: newIndex,
71
77
  type: config.type,
72
78
  isActive: newIndex === 0,
79
+ isPreloaded: newIndex <= 1,
80
+ // Preload first 2 views by default
73
81
  capability: "none",
74
82
  navigation: "unlocked",
75
83
  explicitLock: null,
76
84
  progress: 0,
77
85
  metrics: { scrollHeight: 0, clientHeight: 0, scrollTop: 0 },
78
- config
86
+ config,
87
+ activeSnapPointId: null
79
88
  };
80
89
  const newViews = [...state.views, newView];
81
90
  return {
@@ -116,7 +125,10 @@ var useScrollStore = create()(
116
125
  progress,
117
126
  navigation
118
127
  };
119
- return { views: newViews };
128
+ const activeView = newViews[state.activeIndex];
129
+ const viewProgress = activeView?.progress ?? 0;
130
+ const globalProgress = (state.activeIndex + viewProgress) / state.totalViews;
131
+ return { views: newViews, globalProgress };
120
132
  });
121
133
  },
122
134
  processIntention: (intention) => {
@@ -127,10 +139,15 @@ var useScrollStore = create()(
127
139
  if (intention.type === "navigate") {
128
140
  if (intention.direction === "down") {
129
141
  if (activeView.navigation === "locked") return false;
130
- if (state.activeIndex < state.totalViews - 1) {
131
- get().goToView(state.activeIndex + 1);
132
- return true;
142
+ if (state.activeIndex >= state.totalViews - 1) {
143
+ if (state.infiniteScrollEnabled) {
144
+ get().goToView(0);
145
+ return true;
146
+ }
147
+ return false;
133
148
  }
149
+ get().goToView(state.activeIndex + 1);
150
+ return true;
134
151
  } else if (intention.direction === "up") {
135
152
  const isAtTop = activeView.metrics.scrollTop <= 1;
136
153
  if (activeView.capability === "internal" && !isAtTop) {
@@ -144,21 +161,28 @@ var useScrollStore = create()(
144
161
  } else {
145
162
  if (activeView.explicitLock === "locked") return false;
146
163
  }
147
- if (state.activeIndex > 0) {
148
- get().goToView(state.activeIndex - 1);
149
- return true;
164
+ if (state.activeIndex <= 0) {
165
+ if (state.infiniteScrollEnabled) {
166
+ get().goToView(state.totalViews - 1);
167
+ return true;
168
+ }
169
+ return false;
150
170
  }
171
+ get().goToView(state.activeIndex - 1);
172
+ return true;
151
173
  }
152
174
  }
153
175
  return false;
154
176
  },
155
177
  goToNext: () => {
156
178
  const state = get();
157
- state.goToView(state.activeIndex + 1);
179
+ const nextIndex = state.infiniteScrollEnabled && state.activeIndex >= state.totalViews - 1 ? 0 : state.activeIndex + 1;
180
+ state.goToView(nextIndex);
158
181
  },
159
182
  goToPrevious: () => {
160
183
  const state = get();
161
- state.goToView(state.activeIndex - 1);
184
+ const prevIndex = state.infiniteScrollEnabled && state.activeIndex <= 0 ? state.totalViews - 1 : state.activeIndex - 1;
185
+ state.goToView(prevIndex);
162
186
  },
163
187
  goToView: (indexOrId) => {
164
188
  const state = get();
@@ -166,19 +190,29 @@ var useScrollStore = create()(
166
190
  if (now - lastNavigationTime < NAVIGATION_COOLDOWN) return;
167
191
  lastNavigationTime = now;
168
192
  let targetIndex = typeof indexOrId === "string" ? state.views.findIndex((v) => v.id === indexOrId) : indexOrId;
193
+ if (state.infiniteScrollEnabled) {
194
+ if (targetIndex < 0) targetIndex = state.totalViews - 1;
195
+ if (targetIndex >= state.totalViews) targetIndex = 0;
196
+ }
169
197
  if (targetIndex < 0 || targetIndex >= state.totalViews) return;
170
198
  if (targetIndex === state.activeIndex) return;
171
- set((s) => ({
172
- ...s,
173
- isTransitioning: true,
174
- activeIndex: targetIndex,
175
- activeId: s.views[targetIndex]?.id ?? null,
176
- views: s.views.map((v) => {
177
- if (v.index === targetIndex) return { ...v, isActive: true };
178
- if (v.index === s.activeIndex) return { ...v, isActive: false };
179
- return v;
180
- })
181
- }));
199
+ set((s) => {
200
+ const newViews = s.views.map((v, idx) => {
201
+ const isAdjacent = Math.abs(idx - targetIndex) <= 1 || s.infiniteScrollEnabled && (targetIndex === 0 && idx === s.totalViews - 1 || targetIndex === s.totalViews - 1 && idx === 0);
202
+ return {
203
+ ...v,
204
+ isActive: idx === targetIndex,
205
+ isPreloaded: isAdjacent || idx === targetIndex
206
+ };
207
+ });
208
+ return {
209
+ ...s,
210
+ isTransitioning: true,
211
+ activeIndex: targetIndex,
212
+ activeId: newViews[targetIndex]?.id ?? null,
213
+ views: newViews
214
+ };
215
+ });
182
216
  },
183
217
  setViewExplicitLock: (id, lock) => {
184
218
  set((state) => {
@@ -193,9 +227,33 @@ var useScrollStore = create()(
193
227
  },
194
228
  setGlobalLock: (locked) => set({ isGlobalLocked: locked }),
195
229
  setDragging: (dragging) => set({ isDragging: dragging }),
196
- getViewById: (id) => get().views.find((v) => v.id === id),
197
230
  startTransition: () => set({ isTransitioning: true }),
198
231
  endTransition: () => set({ isTransitioning: false }),
232
+ // NEW: AutoScroll control
233
+ setAutoScrolling: (enabled) => set({ isAutoScrolling: enabled }),
234
+ setAutoScrollPaused: (paused) => set({ isAutoScrollPaused: paused }),
235
+ // NEW: Infinite scroll
236
+ setInfiniteScrollEnabled: (enabled) => set({ infiniteScrollEnabled: enabled }),
237
+ // NEW: Preload
238
+ setViewPreloaded: (id, preloaded) => {
239
+ set((state) => {
240
+ const index = state.views.findIndex((v) => v.id === id);
241
+ if (index === -1) return state;
242
+ const newViews = [...state.views];
243
+ newViews[index] = { ...newViews[index], isPreloaded: preloaded };
244
+ return { views: newViews };
245
+ });
246
+ },
247
+ // NEW: Snap points
248
+ setActiveSnapPoint: (viewId, snapPointId) => {
249
+ set((state) => {
250
+ const index = state.views.findIndex((v) => v.id === viewId);
251
+ if (index === -1) return state;
252
+ const newViews = [...state.views];
253
+ newViews[index] = { ...newViews[index], activeSnapPointId: snapPointId };
254
+ return { views: newViews };
255
+ });
256
+ },
199
257
  resetNavigationCooldown: () => {
200
258
  lastNavigationTime = 0;
201
259
  }
@@ -207,15 +265,16 @@ var selectCanNavigateNext = (state) => {
207
265
  if (state.isTransitioning || state.isGlobalLocked) return false;
208
266
  const activeView = state.views[state.activeIndex];
209
267
  if (!activeView) return false;
210
- return activeView.navigation === "unlocked";
268
+ if (state.infiniteScrollEnabled) return activeView.navigation === "unlocked";
269
+ return activeView.navigation === "unlocked" && state.activeIndex < state.totalViews - 1;
211
270
  };
212
271
  var selectCanNavigatePrevious = (state) => {
213
272
  if (state.isTransitioning || state.isGlobalLocked) return false;
273
+ if (state.infiniteScrollEnabled) return state.activeIndex >= 0;
214
274
  return state.activeIndex > 0;
215
275
  };
216
-
217
- // hooks/useWheelHandler.ts
218
- import { useEffect, useRef } from "react";
276
+ var selectGlobalProgress = (state) => state.globalProgress;
277
+ var selectIsAutoScrolling = (state) => state.isAutoScrolling && !state.isAutoScrollPaused;
219
278
 
220
279
  // utils/normalizeWheel.ts
221
280
  function normalizeWheel(event) {
@@ -254,7 +313,7 @@ function normalizeWheel(event) {
254
313
  function useWheelHandler() {
255
314
  const scrollAccumulator = useRef(0);
256
315
  const lastScrollTime = useRef(0);
257
- const isTransitioning = useScrollStore((s) => s.isTransitioning);
316
+ useScrollStore((s) => s.isTransitioning);
258
317
  useEffect(() => {
259
318
  const handleWheel = (event) => {
260
319
  const state = useScrollStore.getState();
@@ -282,7 +341,6 @@ function useWheelHandler() {
282
341
  if (handled) {
283
342
  scrollAccumulator.current = 0;
284
343
  event.preventDefault();
285
- } else {
286
344
  }
287
345
  }
288
346
  };
@@ -290,14 +348,11 @@ function useWheelHandler() {
290
348
  return () => window.removeEventListener("wheel", handleWheel);
291
349
  }, []);
292
350
  }
293
-
294
- // hooks/useTouchHandler.ts
295
- import { useRef as useRef2, useEffect as useEffect2 } from "react";
296
351
  function useTouchHandler(options = {}) {
297
352
  const { enabled = true } = options;
298
- const touchStart = useRef2(null);
299
- const touchStartTime = useRef2(0);
300
- useEffect2(() => {
353
+ const touchStart = useRef(null);
354
+ const touchStartTime = useRef(0);
355
+ useEffect(() => {
301
356
  if (!enabled) return;
302
357
  const handleTouchStart = (e) => {
303
358
  touchStart.current = {
@@ -338,12 +393,9 @@ function useTouchHandler(options = {}) {
338
393
  };
339
394
  }, [enabled]);
340
395
  }
341
-
342
- // hooks/useKeyboardHandler.ts
343
- import { useEffect as useEffect3 } from "react";
344
396
  function useKeyboardHandler(options = {}) {
345
397
  const { enabled = true, preventDefault = true } = options;
346
- useEffect3(() => {
398
+ useEffect(() => {
347
399
  if (!enabled) return;
348
400
  const handleKeyDown = (e) => {
349
401
  const target = e.target;
@@ -401,13 +453,10 @@ function useKeyboardHandler(options = {}) {
401
453
  };
402
454
  }, [enabled, preventDefault]);
403
455
  }
404
-
405
- // hooks/useHashSync.ts
406
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
407
456
  function useHashSync(options = {}) {
408
457
  const { enabled = true, pushHistory = false, hashPrefix = "" } = options;
409
- const hasInitialized = useRef3(false);
410
- useEffect4(() => {
458
+ const hasInitialized = useRef(false);
459
+ useEffect(() => {
411
460
  if (!enabled) return;
412
461
  const unsubscribe = useScrollStore.subscribe(
413
462
  (state) => state.activeIndex,
@@ -428,7 +477,7 @@ function useHashSync(options = {}) {
428
477
  );
429
478
  return () => unsubscribe();
430
479
  }, [enabled, pushHistory, hashPrefix]);
431
- useEffect4(() => {
480
+ useEffect(() => {
432
481
  if (!enabled) return;
433
482
  const handlePopState = () => {
434
483
  const hash = window.location.hash.slice(1);
@@ -443,7 +492,7 @@ function useHashSync(options = {}) {
443
492
  window.addEventListener("popstate", handlePopState);
444
493
  return () => window.removeEventListener("popstate", handlePopState);
445
494
  }, [enabled, hashPrefix]);
446
- useEffect4(() => {
495
+ useEffect(() => {
447
496
  if (!enabled) return;
448
497
  const checkAndNavigate = () => {
449
498
  const state = useScrollStore.getState();
@@ -466,9 +515,6 @@ function useHashSync(options = {}) {
466
515
  checkAndNavigate();
467
516
  }, [enabled, hashPrefix]);
468
517
  }
469
-
470
- // hooks/useDragHandler.ts
471
- import { useRef as useRef4, useEffect as useEffect5, useCallback, useState } from "react";
472
518
  var DRAG_THRESHOLD = 50;
473
519
  var VELOCITY_THRESHOLD = 0.5;
474
520
  var RESISTANCE_FACTOR = 0.4;
@@ -479,9 +525,9 @@ function useDragHandler(options = {}) {
479
525
  dragOffset: 0,
480
526
  dragDirection: null
481
527
  });
482
- const touchStartRef = useRef4(null);
483
- const lastMoveRef = useRef4(null);
484
- const rafRef = useRef4(null);
528
+ const touchStartRef = useRef(null);
529
+ const lastMoveRef = useRef(null);
530
+ const rafRef = useRef(null);
485
531
  const updateDragState = useCallback((newState) => {
486
532
  setDragState((prev) => {
487
533
  const updated = { ...prev, ...newState };
@@ -489,7 +535,7 @@ function useDragHandler(options = {}) {
489
535
  return updated;
490
536
  });
491
537
  }, [onDragUpdate]);
492
- useEffect5(() => {
538
+ useEffect(() => {
493
539
  if (!enabled) return;
494
540
  const handleTouchStart = (e) => {
495
541
  const target = e.target;
@@ -575,16 +621,13 @@ function useDragHandler(options = {}) {
575
621
  }, [enabled, updateDragState, onDragEnd]);
576
622
  return dragState;
577
623
  }
578
-
579
- // hooks/useFocusManagement.ts
580
- import { useEffect as useEffect6, useRef as useRef5 } from "react";
581
624
  function useFocusManagement(options = {}) {
582
625
  const { enabled = true, focusDelay = 100 } = options;
583
626
  const activeIndex = useScrollStore((s) => s.activeIndex);
584
627
  const activeId = useScrollStore((s) => s.activeId);
585
628
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
586
- const prevIndexRef = useRef5(activeIndex);
587
- useEffect6(() => {
629
+ const prevIndexRef = useRef(activeIndex);
630
+ useEffect(() => {
588
631
  if (!enabled) return;
589
632
  if (activeIndex !== prevIndexRef.current && !isTransitioning && activeId) {
590
633
  const timer = setTimeout(() => {
@@ -601,9 +644,6 @@ function useFocusManagement(options = {}) {
601
644
  }
602
645
  }, [activeIndex, activeId, isTransitioning, enabled, focusDelay]);
603
646
  }
604
-
605
- // hooks/useScrollSystem.ts
606
- import { useCallback as useCallback2, useMemo } from "react";
607
647
  function useScrollSystem() {
608
648
  const activeIndex = useScrollStore((s) => s.activeIndex);
609
649
  const globalProgress = useScrollStore((s) => s.globalProgress);
@@ -623,24 +663,24 @@ function useScrollSystem() {
623
663
  const activeViewProgress = activeView?.progress ?? 0;
624
664
  const canNavigateNext = useScrollStore(selectCanNavigateNext);
625
665
  const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
626
- const goToNext = useCallback2(() => {
666
+ const goToNext = useCallback(() => {
627
667
  if (canNavigateNext) {
628
668
  storeNext();
629
669
  return true;
630
670
  }
631
671
  return false;
632
672
  }, [canNavigateNext, storeNext]);
633
- const goToPrev = useCallback2(() => {
673
+ const goToPrev = useCallback(() => {
634
674
  if (canNavigatePrevious) {
635
675
  storePrev();
636
676
  return true;
637
677
  }
638
678
  return false;
639
679
  }, [canNavigatePrevious, storePrev]);
640
- const getCurrentIndex = useCallback2(() => activeIndex, [activeIndex]);
641
- const getProgress = useCallback2(() => globalProgress, [globalProgress]);
642
- const getActiveViewProgress = useCallback2(() => activeViewProgress, [activeViewProgress]);
643
- const isLocked = useCallback2(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
680
+ const getCurrentIndex = useCallback(() => activeIndex, [activeIndex]);
681
+ const getProgress = useCallback(() => globalProgress, [globalProgress]);
682
+ const getActiveViewProgress = useCallback(() => activeViewProgress, [activeViewProgress]);
683
+ const isLocked = useCallback(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
644
684
  return useMemo(() => ({
645
685
  goToNext,
646
686
  goToPrev,
@@ -677,6 +717,255 @@ function useScrollSystem() {
677
717
  isTransitioning
678
718
  ]);
679
719
  }
720
+ function useGlobalProgress(options = {}) {
721
+ const { onProgress, throttle: throttle2 = 16 } = options;
722
+ const progress = useScrollStore(selectGlobalProgress);
723
+ const activeIndex = useScrollStore((s) => s.activeIndex);
724
+ const totalViews = useScrollStore((s) => s.totalViews);
725
+ useEffect(() => {
726
+ if (!onProgress) return;
727
+ let lastCall = 0;
728
+ const now = Date.now();
729
+ if (now - lastCall >= throttle2) {
730
+ lastCall = now;
731
+ onProgress(progress);
732
+ }
733
+ }, [progress, onProgress, throttle2]);
734
+ return {
735
+ progress,
736
+ activeIndex,
737
+ totalViews,
738
+ percentage: Math.round(progress * 100)
739
+ };
740
+ }
741
+ var DEFAULT_CONFIG = {
742
+ enabled: false,
743
+ interval: 5e3,
744
+ pauseOnInteraction: true,
745
+ resumeDelay: 3e3,
746
+ direction: "forward",
747
+ stopAtEnd: false
748
+ };
749
+ function useAutoScroll(config) {
750
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
751
+ const {
752
+ enabled,
753
+ interval,
754
+ pauseOnInteraction,
755
+ resumeDelay,
756
+ direction,
757
+ stopAtEnd
758
+ } = mergedConfig;
759
+ const intervalRef = useRef(null);
760
+ const resumeTimeoutRef = useRef(null);
761
+ const isAutoScrolling = useScrollStore((s) => s.isAutoScrolling);
762
+ const isAutoScrollPaused = useScrollStore((s) => s.isAutoScrollPaused);
763
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
764
+ const activeIndex = useScrollStore((s) => s.activeIndex);
765
+ const totalViews = useScrollStore((s) => s.totalViews);
766
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
767
+ const isDragging = useScrollStore((s) => s.isDragging);
768
+ const setAutoScrolling = useScrollStore((s) => s.setAutoScrolling);
769
+ const setAutoScrollPaused = useScrollStore((s) => s.setAutoScrollPaused);
770
+ const goToNext = useScrollStore((s) => s.goToNext);
771
+ const goToPrevious = useScrollStore((s) => s.goToPrevious);
772
+ const pause = useCallback(() => {
773
+ setAutoScrollPaused(true);
774
+ if (resumeTimeoutRef.current) {
775
+ clearTimeout(resumeTimeoutRef.current);
776
+ resumeTimeoutRef.current = null;
777
+ }
778
+ }, [setAutoScrollPaused]);
779
+ const resume = useCallback(() => {
780
+ setAutoScrollPaused(false);
781
+ }, [setAutoScrollPaused]);
782
+ const toggle = useCallback(() => {
783
+ if (isAutoScrollPaused) {
784
+ resume();
785
+ } else {
786
+ pause();
787
+ }
788
+ }, [isAutoScrollPaused, pause, resume]);
789
+ const reset = useCallback(() => {
790
+ if (intervalRef.current) {
791
+ clearInterval(intervalRef.current);
792
+ intervalRef.current = null;
793
+ }
794
+ }, []);
795
+ useEffect(() => {
796
+ if (!enabled || !pauseOnInteraction) return;
797
+ if (isDragging || isTransitioning) {
798
+ pause();
799
+ if (resumeTimeoutRef.current) {
800
+ clearTimeout(resumeTimeoutRef.current);
801
+ }
802
+ resumeTimeoutRef.current = setTimeout(() => {
803
+ if (enabled) {
804
+ resume();
805
+ }
806
+ }, resumeDelay);
807
+ }
808
+ return () => {
809
+ if (resumeTimeoutRef.current) {
810
+ clearTimeout(resumeTimeoutRef.current);
811
+ }
812
+ };
813
+ }, [isDragging, isTransitioning, enabled, pauseOnInteraction, resumeDelay, pause, resume]);
814
+ useEffect(() => {
815
+ setAutoScrolling(enabled);
816
+ setAutoScrollPaused(false);
817
+ return () => {
818
+ setAutoScrolling(false);
819
+ };
820
+ }, [enabled, setAutoScrolling, setAutoScrollPaused]);
821
+ useEffect(() => {
822
+ if (!enabled || isAutoScrollPaused || isTransitioning) {
823
+ reset();
824
+ return;
825
+ }
826
+ if (stopAtEnd && !infiniteScrollEnabled) {
827
+ if (direction === "forward" && activeIndex >= totalViews - 1) {
828
+ return;
829
+ }
830
+ if (direction === "backward" && activeIndex <= 0) {
831
+ return;
832
+ }
833
+ }
834
+ intervalRef.current = setInterval(() => {
835
+ if (direction === "forward") {
836
+ goToNext();
837
+ } else {
838
+ goToPrevious();
839
+ }
840
+ }, interval);
841
+ return () => {
842
+ reset();
843
+ };
844
+ }, [
845
+ enabled,
846
+ isAutoScrollPaused,
847
+ isTransitioning,
848
+ interval,
849
+ direction,
850
+ stopAtEnd,
851
+ activeIndex,
852
+ totalViews,
853
+ infiniteScrollEnabled,
854
+ goToNext,
855
+ goToPrevious,
856
+ reset
857
+ ]);
858
+ return {
859
+ isPlaying: isAutoScrolling && !isAutoScrollPaused,
860
+ isPaused: isAutoScrollPaused,
861
+ pause,
862
+ resume,
863
+ toggle,
864
+ reset
865
+ };
866
+ }
867
+ var DEFAULT_CONFIG2 = {
868
+ enabled: false,
869
+ loopDirection: "both"
870
+ };
871
+ function useInfiniteScroll(config = false) {
872
+ const normalizedConfig = typeof config === "boolean" ? { ...DEFAULT_CONFIG2, enabled: config } : { ...DEFAULT_CONFIG2, ...config };
873
+ const { enabled, loopDirection } = normalizedConfig;
874
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
875
+ const setInfiniteScrollEnabled = useScrollStore((s) => s.setInfiniteScrollEnabled);
876
+ const activeIndex = useScrollStore((s) => s.activeIndex);
877
+ const totalViews = useScrollStore((s) => s.totalViews);
878
+ useEffect(() => {
879
+ setInfiniteScrollEnabled(enabled);
880
+ return () => {
881
+ };
882
+ }, [enabled, setInfiniteScrollEnabled]);
883
+ const enable = () => setInfiniteScrollEnabled(true);
884
+ const disable = () => setInfiniteScrollEnabled(false);
885
+ const toggle = () => setInfiniteScrollEnabled(!infiniteScrollEnabled);
886
+ const canLoopForward = infiniteScrollEnabled && (loopDirection === "forward" || loopDirection === "both") && activeIndex === totalViews - 1;
887
+ const canLoopBackward = infiniteScrollEnabled && (loopDirection === "backward" || loopDirection === "both") && activeIndex === 0;
888
+ return {
889
+ isEnabled: infiniteScrollEnabled,
890
+ enable,
891
+ disable,
892
+ toggle,
893
+ canLoopForward,
894
+ canLoopBackward
895
+ };
896
+ }
897
+ var DEFAULT_CONFIG3 = {
898
+ ahead: 1,
899
+ behind: 1,
900
+ delay: 100
901
+ };
902
+ function usePreload(config = {}) {
903
+ const mergedConfig = { ...DEFAULT_CONFIG3, ...config };
904
+ const { ahead, behind, delay } = mergedConfig;
905
+ const views = useScrollStore((s) => s.views);
906
+ const activeIndex = useScrollStore((s) => s.activeIndex);
907
+ const totalViews = useScrollStore((s) => s.totalViews);
908
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
909
+ const setViewPreloaded = useScrollStore((s) => s.setViewPreloaded);
910
+ const preloadedViewIds = useMemo(() => {
911
+ const ids = [];
912
+ for (let i = -behind; i <= ahead; i++) {
913
+ let targetIndex = activeIndex + i;
914
+ if (infiniteScrollEnabled) {
915
+ if (targetIndex < 0) targetIndex = totalViews + targetIndex;
916
+ if (targetIndex >= totalViews) targetIndex = targetIndex - totalViews;
917
+ }
918
+ if (targetIndex >= 0 && targetIndex < totalViews) {
919
+ const view = views[targetIndex];
920
+ if (view) {
921
+ ids.push(view.id);
922
+ }
923
+ }
924
+ }
925
+ return ids;
926
+ }, [activeIndex, ahead, behind, views, totalViews, infiniteScrollEnabled]);
927
+ useEffect(() => {
928
+ const timer = setTimeout(() => {
929
+ views.forEach((view) => {
930
+ const shouldBePreloaded = preloadedViewIds.includes(view.id);
931
+ if (view.isPreloaded !== shouldBePreloaded) {
932
+ setViewPreloaded(view.id, shouldBePreloaded);
933
+ }
934
+ });
935
+ }, delay);
936
+ return () => clearTimeout(timer);
937
+ }, [preloadedViewIds, views, setViewPreloaded, delay]);
938
+ const shouldPreload = (viewId) => {
939
+ return preloadedViewIds.includes(viewId);
940
+ };
941
+ const isPreloaded = (viewId) => {
942
+ const view = views.find((v) => v.id === viewId);
943
+ return view?.isPreloaded ?? false;
944
+ };
945
+ return {
946
+ shouldPreload,
947
+ preloadedViewIds,
948
+ isPreloaded
949
+ };
950
+ }
951
+ var DEFAULT_GESTURE_CONFIG = {
952
+ swipeThreshold: 50,
953
+ swipeVelocity: 0.5,
954
+ dragResistance: 0.3,
955
+ enableWheel: true,
956
+ enableTouch: true,
957
+ enableKeyboard: true
958
+ };
959
+ var GestureConfigContext = createContext(DEFAULT_GESTURE_CONFIG);
960
+ function useGestureConfig() {
961
+ return useContext(GestureConfigContext);
962
+ }
963
+ function mergeGestureConfig(config) {
964
+ return useMemo(() => ({
965
+ ...DEFAULT_GESTURE_CONFIG,
966
+ ...config
967
+ }), [config]);
968
+ }
680
969
 
681
970
  // utils/index.ts
682
971
  function throttle(fn, limit) {
@@ -705,9 +994,6 @@ function prefersReducedMotion() {
705
994
  if (typeof window === "undefined") return false;
706
995
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
707
996
  }
708
-
709
- // components/ScrollContainer.tsx
710
- import { jsx } from "react/jsx-runtime";
711
997
  function ScrollContainer({
712
998
  children,
713
999
  className = "",
@@ -725,12 +1011,20 @@ function ScrollContainer({
725
1011
  // Touch Physics
726
1012
  enableDragPhysics = false,
727
1013
  // Layout
728
- orientation = "vertical"
1014
+ orientation = "vertical",
1015
+ // NEW: v1.1.0 Features
1016
+ skipInitialAnimation = false,
1017
+ onProgress,
1018
+ gestureConfig,
1019
+ autoScroll,
1020
+ infiniteScroll = false,
1021
+ preload = true
729
1022
  }) {
730
- const containerRef = useRef6(null);
1023
+ const containerRef = useRef(null);
1024
+ const isFirstRender = useRef(true);
731
1025
  const isBrowser = typeof window !== "undefined";
732
- const [reducedMotion, setReducedMotion] = useState2(false);
733
- useEffect7(() => {
1026
+ const [reducedMotion, setReducedMotion] = useState(false);
1027
+ useEffect(() => {
734
1028
  if (!isBrowser || !respectReducedMotion) return;
735
1029
  setReducedMotion(prefersReducedMotion());
736
1030
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
@@ -741,9 +1035,9 @@ function ScrollContainer({
741
1035
  const effectiveDuration = reducedMotion ? 0 : transitionDuration;
742
1036
  const { initialize, endTransition } = useScrollStore();
743
1037
  const activeIndex = useScrollStore((s) => s.activeIndex);
744
- const totalViews = useScrollStore((s) => s.totalViews);
745
- const isInitialized = useScrollStore((s) => s.isInitialized);
746
- const prevIndexRef = useRef6(activeIndex);
1038
+ useScrollStore((s) => s.totalViews);
1039
+ useScrollStore((s) => s.isInitialized);
1040
+ const prevIndexRef = useRef(activeIndex);
747
1041
  useWheelHandler();
748
1042
  useTouchHandler({ enabled: !enableDragPhysics });
749
1043
  useKeyboardHandler();
@@ -756,14 +1050,39 @@ function ScrollContainer({
756
1050
  hashPrefix
757
1051
  });
758
1052
  useFocusManagement({ enabled: enableFocusManagement });
759
- useEffect7(() => {
1053
+ useGlobalProgress({ onProgress });
1054
+ useInfiniteScroll(infiniteScroll);
1055
+ const autoScrollState = useAutoScroll(
1056
+ autoScroll ?? { enabled: false, interval: 5e3 }
1057
+ );
1058
+ const preloadConfig = typeof preload === "boolean" ? preload ? { ahead: 1, behind: 1 } : void 0 : preload;
1059
+ usePreload(preloadConfig ?? {});
1060
+ const mergedGestureConfig = mergeGestureConfig(gestureConfig);
1061
+ useEffect(() => {
760
1062
  const timer = setTimeout(() => {
761
1063
  initialize();
762
1064
  onInitialized?.();
1065
+ isFirstRender.current = false;
763
1066
  }, 50);
764
1067
  return () => clearTimeout(timer);
765
1068
  }, [initialize, onInitialized]);
766
- useEffect7(() => {
1069
+ useEffect(() => {
1070
+ if (!isBrowser) return;
1071
+ const html = document.documentElement;
1072
+ const body = document.body;
1073
+ const originalHtmlOverscroll = html.style.overscrollBehavior;
1074
+ const originalBodyOverscroll = body.style.overscrollBehavior;
1075
+ const originalTouchAction = body.style.touchAction;
1076
+ html.style.overscrollBehavior = "none";
1077
+ body.style.overscrollBehavior = "none";
1078
+ body.style.touchAction = "pan-x pan-y";
1079
+ return () => {
1080
+ html.style.overscrollBehavior = originalHtmlOverscroll;
1081
+ body.style.overscrollBehavior = originalBodyOverscroll;
1082
+ body.style.touchAction = originalTouchAction;
1083
+ };
1084
+ }, [isBrowser]);
1085
+ useEffect(() => {
767
1086
  if (prevIndexRef.current !== activeIndex) {
768
1087
  onViewChange?.(prevIndexRef.current, activeIndex);
769
1088
  const timer = setTimeout(() => {
@@ -773,37 +1092,34 @@ function ScrollContainer({
773
1092
  return () => clearTimeout(timer);
774
1093
  }
775
1094
  }, [activeIndex, effectiveDuration, onViewChange, endTransition]);
776
- const wrapperStyle = useMemo2(() => {
1095
+ const wrapperStyle = useMemo(() => {
777
1096
  const baseOffset = activeIndex * 100;
778
1097
  const dragOffset = dragState.isDragging ? dragState.dragOffset * 100 : 0;
779
1098
  const transformAxis = orientation === "horizontal" ? "X" : "Y";
780
1099
  const sizeUnit = orientation === "horizontal" ? "vw" : "vh";
1100
+ const shouldAnimate = !skipInitialAnimation || !isFirstRender.current;
1101
+ const transitionValue = dragState.isDragging ? "none" : shouldAnimate && effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none";
781
1102
  return {
782
1103
  transform: `translate${transformAxis}(-${baseOffset + dragOffset}${sizeUnit})`,
783
- transition: dragState.isDragging ? "none" : effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none",
1104
+ transition: transitionValue,
784
1105
  height: "100%",
785
1106
  width: "100%",
786
1107
  display: orientation === "horizontal" ? "flex" : "block",
787
1108
  flexDirection: orientation === "horizontal" ? "row" : void 0
788
1109
  };
789
- }, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation]);
790
- return /* @__PURE__ */ jsx(
1110
+ }, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation, skipInitialAnimation]);
1111
+ return /* @__PURE__ */ jsx(GestureConfigContext.Provider, { value: mergedGestureConfig, children: /* @__PURE__ */ jsx(
791
1112
  "div",
792
1113
  {
793
1114
  ref: containerRef,
794
1115
  className: `scroll-container fixed inset-0 overflow-hidden w-screen h-screen ${className}`,
795
1116
  role: "main",
796
1117
  "aria-label": "Scroll container",
1118
+ "data-auto-scrolling": autoScrollState.isPlaying,
797
1119
  children: /* @__PURE__ */ jsx("div", { className: "scroll-wrapper", style: wrapperStyle, children })
798
1120
  }
799
- );
1121
+ ) });
800
1122
  }
801
-
802
- // components/FullView.tsx
803
- import { useMemo as useMemo3 } from "react";
804
-
805
- // hooks/useViewRegistration.ts
806
- import { useEffect as useEffect8, useRef as useRef7 } from "react";
807
1123
  function useViewRegistration({
808
1124
  config,
809
1125
  onActivate,
@@ -817,7 +1133,7 @@ function useViewRegistration({
817
1133
  const unregisterView = useScrollStore((s) => s.unregisterView);
818
1134
  const activeId = useScrollStore((s) => s.activeId);
819
1135
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
820
- const callbacksRef = useRef7({
1136
+ const callbacksRef = useRef({
821
1137
  onActivate,
822
1138
  onDeactivate,
823
1139
  onEnterStart,
@@ -833,20 +1149,20 @@ function useViewRegistration({
833
1149
  onExitStart,
834
1150
  onExitEnd
835
1151
  };
836
- const wasActiveRef = useRef7(false);
837
- const wasTransitioningRef = useRef7(false);
838
- const configRef = useRef7(config);
1152
+ const wasActiveRef = useRef(false);
1153
+ const wasTransitioningRef = useRef(false);
1154
+ const configRef = useRef(config);
839
1155
  if (JSON.stringify(config) !== JSON.stringify(configRef.current)) {
840
1156
  configRef.current = config;
841
1157
  }
842
- useEffect8(() => {
1158
+ useEffect(() => {
843
1159
  const currentConfig = configRef.current;
844
1160
  registerView(currentConfig);
845
1161
  return () => unregisterView(currentConfig.id);
846
1162
  }, [registerView, unregisterView, configRef.current]);
847
1163
  const viewState = useScrollStore((s) => s.views.find((v) => v.id === config.id));
848
1164
  const isActive = activeId === config.id;
849
- useEffect8(() => {
1165
+ useEffect(() => {
850
1166
  if (isActive && !wasActiveRef.current) {
851
1167
  callbacksRef.current.onActivate?.();
852
1168
  } else if (!isActive && wasActiveRef.current) {
@@ -854,7 +1170,7 @@ function useViewRegistration({
854
1170
  }
855
1171
  wasActiveRef.current = isActive;
856
1172
  }, [isActive]);
857
- useEffect8(() => {
1173
+ useEffect(() => {
858
1174
  const callbacks = callbacksRef.current;
859
1175
  const wasActive = wasActiveRef.current;
860
1176
  const wasTransitioning = wasTransitioningRef.current;
@@ -882,9 +1198,6 @@ function useViewRegistration({
882
1198
  navigation: viewState?.navigation ?? "unlocked"
883
1199
  };
884
1200
  }
885
-
886
- // components/FullView.tsx
887
- import { jsx as jsx2 } from "react/jsx-runtime";
888
1201
  function FullView({
889
1202
  id,
890
1203
  children,
@@ -897,7 +1210,7 @@ function FullView({
897
1210
  onExitStart,
898
1211
  onExitEnd
899
1212
  }) {
900
- const config = useMemo3(
1213
+ const config = useMemo(
901
1214
  () => ({
902
1215
  id,
903
1216
  type: "full",
@@ -914,7 +1227,7 @@ function FullView({
914
1227
  onExitStart,
915
1228
  onExitEnd
916
1229
  });
917
- return /* @__PURE__ */ jsx2(
1230
+ return /* @__PURE__ */ jsx(
918
1231
  "section",
919
1232
  {
920
1233
  id,
@@ -934,9 +1247,6 @@ function FullView({
934
1247
  }
935
1248
  );
936
1249
  }
937
-
938
- // hooks/useMetricsReporter.ts
939
- import { useRef as useRef8, useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4 } from "react";
940
1250
  var SCROLL_THROTTLE_MS = 66;
941
1251
  function useMetricsReporter({
942
1252
  id,
@@ -945,9 +1255,9 @@ function useMetricsReporter({
945
1255
  onScrollProgress,
946
1256
  throttleMs = SCROLL_THROTTLE_MS
947
1257
  }) {
948
- const scrollRef = useRef8(null);
1258
+ const scrollRef = useRef(null);
949
1259
  const updateMetrics = useScrollStore((s) => s.updateViewMetrics);
950
- const measureAndReport = useCallback3(() => {
1260
+ const measureAndReport = useCallback(() => {
951
1261
  const el = scrollRef.current;
952
1262
  if (!el) return;
953
1263
  const metrics = {
@@ -962,11 +1272,11 @@ function useMetricsReporter({
962
1272
  onScrollProgress(progress);
963
1273
  }
964
1274
  }, [id, scrollDirection, updateMetrics, onScrollProgress]);
965
- const throttledMeasure = useMemo4(
1275
+ const throttledMeasure = useMemo(
966
1276
  () => throttle(measureAndReport, throttleMs),
967
1277
  [measureAndReport, throttleMs]
968
1278
  );
969
- useEffect9(() => {
1279
+ useEffect(() => {
970
1280
  const el = scrollRef.current;
971
1281
  if (!el) return;
972
1282
  const resizeObserver = new ResizeObserver(() => {
@@ -981,16 +1291,13 @@ function useMetricsReporter({
981
1291
  el.removeEventListener("scroll", throttledMeasure);
982
1292
  };
983
1293
  }, [measureAndReport, throttledMeasure]);
984
- useEffect9(() => {
1294
+ useEffect(() => {
985
1295
  if (isActive) {
986
1296
  measureAndReport();
987
1297
  }
988
1298
  }, [isActive, measureAndReport]);
989
1299
  return { scrollRef, measureAndReport };
990
1300
  }
991
-
992
- // components/ScrollLockedView.tsx
993
- import { jsx as jsx3 } from "react/jsx-runtime";
994
1301
  function ScrollLockedView({
995
1302
  id,
996
1303
  children,
@@ -1026,14 +1333,14 @@ function ScrollLockedView({
1026
1333
  onScrollProgress
1027
1334
  });
1028
1335
  const scrollClasses = scrollDirection === "vertical" ? "overflow-y-auto overflow-x-hidden" : "overflow-x-auto overflow-y-hidden";
1029
- return /* @__PURE__ */ jsx3(
1336
+ return /* @__PURE__ */ jsx(
1030
1337
  "section",
1031
1338
  {
1032
1339
  id,
1033
1340
  className: `relative w-full h-screen ${className}`,
1034
1341
  "data-view-type": "scroll-locked",
1035
1342
  "data-active": isActive,
1036
- children: /* @__PURE__ */ jsx3(
1343
+ children: /* @__PURE__ */ jsx(
1037
1344
  "div",
1038
1345
  {
1039
1346
  ref: scrollRef,
@@ -1045,10 +1352,6 @@ function ScrollLockedView({
1045
1352
  }
1046
1353
  );
1047
1354
  }
1048
-
1049
- // components/ControlledView.tsx
1050
- import { useMemo as useMemo5, useEffect as useEffect10 } from "react";
1051
- import { jsx as jsx4 } from "react/jsx-runtime";
1052
1355
  function ControlledView({
1053
1356
  id,
1054
1357
  children,
@@ -1065,7 +1368,7 @@ function ControlledView({
1065
1368
  onExitEnd
1066
1369
  }) {
1067
1370
  const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1068
- const config = useMemo5(
1371
+ const config = useMemo(
1069
1372
  () => ({
1070
1373
  id,
1071
1374
  type: "controlled",
@@ -1089,11 +1392,11 @@ function ControlledView({
1089
1392
  isActive,
1090
1393
  scrollDirection: allowInternalScroll ? scrollDirection : "none"
1091
1394
  });
1092
- useEffect10(() => {
1395
+ useEffect(() => {
1093
1396
  const lockState = canProceed ? "unlocked" : "locked";
1094
1397
  setExplicitLock(id, lockState);
1095
1398
  }, [id, canProceed, setExplicitLock]);
1096
- const overflowClasses = useMemo5(() => {
1399
+ const overflowClasses = useMemo(() => {
1097
1400
  if (!allowInternalScroll) return "overflow-hidden";
1098
1401
  switch (scrollDirection) {
1099
1402
  case "vertical":
@@ -1104,14 +1407,14 @@ function ControlledView({
1104
1407
  return "overflow-auto";
1105
1408
  }
1106
1409
  }, [allowInternalScroll, scrollDirection]);
1107
- return /* @__PURE__ */ jsx4(
1410
+ return /* @__PURE__ */ jsx(
1108
1411
  "section",
1109
1412
  {
1110
1413
  id,
1111
1414
  className: `relative w-full h-screen ${isActive ? "z-10" : "z-0"} ${className}`,
1112
1415
  "data-view-type": "controlled",
1113
1416
  "data-active": isActive,
1114
- children: /* @__PURE__ */ jsx4(
1417
+ children: /* @__PURE__ */ jsx(
1115
1418
  "div",
1116
1419
  {
1117
1420
  ref: scrollRef,
@@ -1127,7 +1430,7 @@ function useViewControl(viewId) {
1127
1430
  const goToNext = useScrollStore((s) => s.goToNext);
1128
1431
  const goToPrevious = useScrollStore((s) => s.goToPrevious);
1129
1432
  const goToView = useScrollStore((s) => s.goToView);
1130
- return useMemo5(
1433
+ return useMemo(
1131
1434
  () => ({
1132
1435
  unlock: () => setExplicitLock(viewId, "unlocked"),
1133
1436
  lock: () => setExplicitLock(viewId, "locked"),
@@ -1139,9 +1442,6 @@ function useViewControl(viewId) {
1139
1442
  [viewId, setExplicitLock, goToNext, goToPrevious, goToView]
1140
1443
  );
1141
1444
  }
1142
-
1143
- // components/ScrollDebugOverlay.tsx
1144
- import { jsx as jsx5, jsxs } from "react/jsx-runtime";
1145
1445
  function ScrollDebugOverlay({
1146
1446
  position = "bottom-left",
1147
1447
  visible = true
@@ -1167,29 +1467,29 @@ function ScrollDebugOverlay({
1167
1467
  style: { pointerEvents: "none" },
1168
1468
  children: [
1169
1469
  /* @__PURE__ */ jsxs("div", { className: "text-green-300 font-bold mb-2 flex items-center gap-2", children: [
1170
- /* @__PURE__ */ jsx5("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
1470
+ /* @__PURE__ */ jsx("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
1171
1471
  "ScrollSystem Debug"
1172
1472
  ] }),
1173
1473
  /* @__PURE__ */ jsxs("div", { className: "space-y-1 mb-3", children: [
1174
- /* @__PURE__ */ jsx5(Row, { label: "initialized", value: isInitialized }),
1175
- /* @__PURE__ */ jsx5(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
1176
- /* @__PURE__ */ jsx5(Row, { label: "transitioning", value: isTransitioning }),
1177
- /* @__PURE__ */ jsx5(Row, { label: "globalLocked", value: isGlobalLocked })
1474
+ /* @__PURE__ */ jsx(Row, { label: "initialized", value: isInitialized }),
1475
+ /* @__PURE__ */ jsx(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
1476
+ /* @__PURE__ */ jsx(Row, { label: "transitioning", value: isTransitioning }),
1477
+ /* @__PURE__ */ jsx(Row, { label: "globalLocked", value: isGlobalLocked })
1178
1478
  ] }),
1179
1479
  activeView && /* @__PURE__ */ jsxs("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
1180
- /* @__PURE__ */ jsx5("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
1181
- /* @__PURE__ */ jsx5(Row, { label: "id", value: activeView.id }),
1182
- /* @__PURE__ */ jsx5(Row, { label: "type", value: activeView.type }),
1183
- /* @__PURE__ */ jsx5(Row, { label: "capability", value: activeView.capability }),
1184
- /* @__PURE__ */ jsx5(Row, { label: "navigation", value: activeView.navigation }),
1185
- /* @__PURE__ */ jsx5(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
1186
- activeView.explicitLock && /* @__PURE__ */ jsx5(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
1480
+ /* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
1481
+ /* @__PURE__ */ jsx(Row, { label: "id", value: activeView.id }),
1482
+ /* @__PURE__ */ jsx(Row, { label: "type", value: activeView.type }),
1483
+ /* @__PURE__ */ jsx(Row, { label: "capability", value: activeView.capability }),
1484
+ /* @__PURE__ */ jsx(Row, { label: "navigation", value: activeView.navigation }),
1485
+ /* @__PURE__ */ jsx(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
1486
+ activeView.explicitLock && /* @__PURE__ */ jsx(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
1187
1487
  ] }),
1188
1488
  activeView?.metrics && /* @__PURE__ */ jsxs("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
1189
- /* @__PURE__ */ jsx5("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
1190
- /* @__PURE__ */ jsx5(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
1191
- /* @__PURE__ */ jsx5(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
1192
- /* @__PURE__ */ jsx5(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
1489
+ /* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
1490
+ /* @__PURE__ */ jsx(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
1491
+ /* @__PURE__ */ jsx(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
1492
+ /* @__PURE__ */ jsx(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
1193
1493
  ] })
1194
1494
  ]
1195
1495
  }
@@ -1207,23 +1507,19 @@ function Row({
1207
1507
  label,
1208
1508
  ":"
1209
1509
  ] }),
1210
- /* @__PURE__ */ jsx5("span", { className: valueColor, children: displayValue })
1510
+ /* @__PURE__ */ jsx("span", { className: valueColor, children: displayValue })
1211
1511
  ] });
1212
1512
  }
1213
-
1214
- // components/AriaLiveRegion.tsx
1215
- import { useEffect as useEffect11, useState as useState3 } from "react";
1216
- import { jsx as jsx6 } from "react/jsx-runtime";
1217
1513
  function AriaLiveRegion({
1218
1514
  template = "Navigated to section {viewIndex} of {totalViews}",
1219
1515
  politeness = "polite"
1220
1516
  }) {
1221
- const [announcement, setAnnouncement] = useState3("");
1517
+ const [announcement, setAnnouncement] = useState("");
1222
1518
  const activeIndex = useScrollStore((s) => s.activeIndex);
1223
1519
  const activeId = useScrollStore((s) => s.activeId);
1224
1520
  const totalViews = useScrollStore((s) => s.totalViews);
1225
1521
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
1226
- useEffect11(() => {
1522
+ useEffect(() => {
1227
1523
  if (!isTransitioning && activeId) {
1228
1524
  const message = template.replace("{viewIndex}", String(activeIndex + 1)).replace("{viewId}", activeId).replace("{totalViews}", String(totalViews));
1229
1525
  const timer = setTimeout(() => {
@@ -1232,7 +1528,7 @@ function AriaLiveRegion({
1232
1528
  return () => clearTimeout(timer);
1233
1529
  }
1234
1530
  }, [activeIndex, activeId, totalViews, isTransitioning, template]);
1235
- return /* @__PURE__ */ jsx6(
1531
+ return /* @__PURE__ */ jsx(
1236
1532
  "div",
1237
1533
  {
1238
1534
  role: "status",
@@ -1254,10 +1550,6 @@ function AriaLiveRegion({
1254
1550
  }
1255
1551
  );
1256
1552
  }
1257
-
1258
- // components/LazyView.tsx
1259
- import { useMemo as useMemo6 } from "react";
1260
- import { Fragment, jsx as jsx7 } from "react/jsx-runtime";
1261
1553
  function LazyView({
1262
1554
  viewId,
1263
1555
  buffer = 1,
@@ -1266,7 +1558,7 @@ function LazyView({
1266
1558
  }) {
1267
1559
  const activeIndex = useScrollStore((s) => s.activeIndex);
1268
1560
  const views = useScrollStore((s) => s.views);
1269
- const shouldRender = useMemo6(() => {
1561
+ const shouldRender = useMemo(() => {
1270
1562
  const viewIndex = views.findIndex((v) => v.id === viewId);
1271
1563
  if (viewIndex === -1) return false;
1272
1564
  const minIndex = Math.max(0, activeIndex - buffer);
@@ -1274,20 +1566,149 @@ function LazyView({
1274
1566
  return viewIndex >= minIndex && viewIndex <= maxIndex;
1275
1567
  }, [viewId, views, activeIndex, buffer]);
1276
1568
  if (!shouldRender) {
1277
- return /* @__PURE__ */ jsx7(Fragment, { children: placeholder });
1569
+ return /* @__PURE__ */ jsx(Fragment, { children: placeholder });
1278
1570
  }
1279
- return /* @__PURE__ */ jsx7(Fragment, { children });
1571
+ return /* @__PURE__ */ jsx(Fragment, { children });
1572
+ }
1573
+ function NestedScrollView({
1574
+ id,
1575
+ children,
1576
+ className = "",
1577
+ nestedDirection = "horizontal",
1578
+ enableSnap = true,
1579
+ onItemChange,
1580
+ onActivate,
1581
+ onDeactivate,
1582
+ onEnterStart,
1583
+ onEnterEnd,
1584
+ onExitStart,
1585
+ onExitEnd
1586
+ }) {
1587
+ const containerRef = useRef(null);
1588
+ const nestedContainerRef = useRef(null);
1589
+ const [activeNestedIndex, setActiveNestedIndex] = useState(0);
1590
+ const [isNestedScrolling, setIsNestedScrolling] = useState(false);
1591
+ useScrollStore((s) => s.activeIndex);
1592
+ const views = useScrollStore((s) => s.views);
1593
+ const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1594
+ const view = views.find((v) => v.id === id);
1595
+ const isActive = view?.isActive ?? false;
1596
+ useViewRegistration({
1597
+ config: {
1598
+ id,
1599
+ type: "nested",
1600
+ nestedConfig: {
1601
+ direction: nestedDirection,
1602
+ enableSnap,
1603
+ onItemChange
1604
+ }
1605
+ },
1606
+ onActivate,
1607
+ onDeactivate,
1608
+ onEnterStart,
1609
+ onEnterEnd,
1610
+ onExitStart,
1611
+ onExitEnd
1612
+ });
1613
+ useEffect(() => {
1614
+ if (isActive) {
1615
+ onActivate?.();
1616
+ } else {
1617
+ onDeactivate?.();
1618
+ }
1619
+ }, [isActive, onActivate, onDeactivate]);
1620
+ const handleNestedScroll = useCallback((e) => {
1621
+ if (!nestedContainerRef.current) return;
1622
+ const container = nestedContainerRef.current;
1623
+ const scrollPos = nestedDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
1624
+ const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
1625
+ if (itemSize > 0) {
1626
+ const newIndex = Math.round(scrollPos / itemSize);
1627
+ if (newIndex !== activeNestedIndex) {
1628
+ setActiveNestedIndex(newIndex);
1629
+ onItemChange?.(newIndex);
1630
+ }
1631
+ }
1632
+ }, [nestedDirection, activeNestedIndex, onItemChange]);
1633
+ const handleTouchStart = useCallback((e) => {
1634
+ setIsNestedScrolling(true);
1635
+ setGlobalLock(true);
1636
+ }, [setGlobalLock]);
1637
+ const handleTouchEnd = useCallback(() => {
1638
+ setTimeout(() => {
1639
+ setIsNestedScrolling(false);
1640
+ setGlobalLock(false);
1641
+ }, 100);
1642
+ }, [setGlobalLock]);
1643
+ useCallback((index) => {
1644
+ if (!nestedContainerRef.current) return;
1645
+ const container = nestedContainerRef.current;
1646
+ const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
1647
+ const scrollPos = index * itemSize;
1648
+ container.scrollTo({
1649
+ [nestedDirection === "horizontal" ? "left" : "top"]: scrollPos,
1650
+ behavior: "smooth"
1651
+ });
1652
+ }, [nestedDirection]);
1653
+ const nestedScrollStyle = {
1654
+ display: "flex",
1655
+ flexDirection: nestedDirection === "horizontal" ? "row" : "column",
1656
+ overflow: nestedDirection === "horizontal" ? "auto hidden" : "hidden auto",
1657
+ scrollSnapType: enableSnap ? `${nestedDirection === "horizontal" ? "x" : "y"} mandatory` : "none",
1658
+ scrollBehavior: "smooth",
1659
+ WebkitOverflowScrolling: "touch",
1660
+ width: "100%",
1661
+ height: "100%"
1662
+ };
1663
+ return /* @__PURE__ */ jsx(
1664
+ "div",
1665
+ {
1666
+ ref: containerRef,
1667
+ className: `scroll-view nested-scroll-view h-screen w-screen flex-shrink-0 relative ${className}`,
1668
+ "data-view-id": id,
1669
+ "data-view-type": "nested",
1670
+ "data-nested-direction": nestedDirection,
1671
+ role: "region",
1672
+ "aria-label": `Nested scroll view ${id}`,
1673
+ tabIndex: 0,
1674
+ children: /* @__PURE__ */ jsx(
1675
+ "div",
1676
+ {
1677
+ ref: nestedContainerRef,
1678
+ className: "nested-scroll-container",
1679
+ style: nestedScrollStyle,
1680
+ onScroll: handleNestedScroll,
1681
+ onTouchStart: handleTouchStart,
1682
+ onTouchEnd: handleTouchEnd,
1683
+ children
1684
+ }
1685
+ )
1686
+ }
1687
+ );
1688
+ }
1689
+ function NestedScrollItem({
1690
+ children,
1691
+ className = ""
1692
+ }) {
1693
+ return /* @__PURE__ */ jsx(
1694
+ "div",
1695
+ {
1696
+ className: `nested-scroll-item flex-shrink-0 w-full h-full ${className}`,
1697
+ style: {
1698
+ scrollSnapAlign: "start",
1699
+ scrollSnapStop: "always"
1700
+ },
1701
+ children
1702
+ }
1703
+ );
1280
1704
  }
1281
-
1282
- // hooks/useNavigation.ts
1283
- import { useCallback as useCallback4, useMemo as useMemo7 } from "react";
1284
1705
  function useNavigation() {
1285
1706
  const goToView = useScrollStore((s) => s.goToView);
1286
1707
  const goToNextAction = useScrollStore((s) => s.goToNext);
1287
1708
  const goToPreviousAction = useScrollStore((s) => s.goToPrevious);
1288
1709
  const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1289
- const lockScroll = useCallback4(() => setGlobalLock(true), [setGlobalLock]);
1290
- const unlockScroll = useCallback4(() => setGlobalLock(false), [setGlobalLock]);
1710
+ const lockScroll = useCallback(() => setGlobalLock(true), [setGlobalLock]);
1711
+ const unlockScroll = useCallback(() => setGlobalLock(false), [setGlobalLock]);
1291
1712
  const activeIndex = useScrollStore((s) => s.activeIndex);
1292
1713
  const activeId = useScrollStore((s) => s.activeId);
1293
1714
  const totalViews = useScrollStore((s) => s.totalViews);
@@ -1295,16 +1716,16 @@ function useNavigation() {
1295
1716
  const isScrollLocked = useScrollStore((s) => s.isGlobalLocked);
1296
1717
  const canNavigateNext = useScrollStore(selectCanNavigateNext);
1297
1718
  const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
1298
- const goToNext = useCallback4(() => {
1719
+ const goToNext = useCallback(() => {
1299
1720
  return goToNextAction();
1300
1721
  }, [goToNextAction]);
1301
- const goToPrevious = useCallback4(() => {
1722
+ const goToPrevious = useCallback(() => {
1302
1723
  return goToPreviousAction();
1303
1724
  }, [goToPreviousAction]);
1304
1725
  const isFirstView = activeIndex === 0;
1305
1726
  const isLastView = activeIndex === totalViews - 1;
1306
1727
  const progress = totalViews > 1 ? activeIndex / (totalViews - 1) : 0;
1307
- return useMemo7(
1728
+ return useMemo(
1308
1729
  () => ({
1309
1730
  // Acciones de navegación
1310
1731
  goToView,
@@ -1374,18 +1795,15 @@ function useActiveViewProgress() {
1374
1795
  viewType: activeView?.type
1375
1796
  };
1376
1797
  }
1377
-
1378
- // hooks/useScrollAnalytics.ts
1379
- import { useEffect as useEffect12, useRef as useRef9, useCallback as useCallback5 } from "react";
1380
1798
  function useScrollAnalytics(options = {}) {
1381
1799
  const { onViewEnter, onViewExit, enabled = true } = options;
1382
1800
  const activeIndex = useScrollStore((s) => s.activeIndex);
1383
1801
  const activeId = useScrollStore((s) => s.activeId);
1384
1802
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
1385
- const enterTimeRef = useRef9(Date.now());
1386
- const prevIndexRef = useRef9(activeIndex);
1387
- const prevIdRef = useRef9(activeId);
1388
- const createAnalytics = useCallback5((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
1803
+ const enterTimeRef = useRef(Date.now());
1804
+ const prevIndexRef = useRef(activeIndex);
1805
+ const prevIdRef = useRef(activeId);
1806
+ const createAnalytics = useCallback((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
1389
1807
  viewId,
1390
1808
  viewIndex,
1391
1809
  enterTime,
@@ -1393,7 +1811,7 @@ function useScrollAnalytics(options = {}) {
1393
1811
  duration: exitTime ? (exitTime - enterTime) / 1e3 : 0,
1394
1812
  isActive
1395
1813
  }), []);
1396
- useEffect12(() => {
1814
+ useEffect(() => {
1397
1815
  if (!enabled) return;
1398
1816
  if (!isTransitioning && (activeIndex !== prevIndexRef.current || activeId !== prevIdRef.current)) {
1399
1817
  const now = Date.now();
@@ -1428,36 +1846,205 @@ function useScrollAnalytics(options = {}) {
1428
1846
  getTimeInView: () => (Date.now() - enterTimeRef.current) / 1e3
1429
1847
  };
1430
1848
  }
1431
- export {
1432
- AriaLiveRegion,
1433
- ControlledView,
1434
- DEFAULT_PROGRESS_DEBOUNCE,
1435
- DEFAULT_TRANSITION_DURATION,
1436
- DEFAULT_TRANSITION_EASING,
1437
- FullView,
1438
- LazyView,
1439
- NAVIGATION_COOLDOWN,
1440
- NAV_THRESHOLDS,
1441
- ScrollContainer,
1442
- ScrollDebugOverlay,
1443
- ScrollLockedView,
1444
- selectActiveView,
1445
- selectActiveViewProgress,
1446
- selectCanNavigateNext,
1447
- selectCanNavigatePrevious,
1448
- useActiveViewProgress,
1449
- useDragHandler,
1450
- useFocusManagement,
1451
- useHashSync,
1452
- useKeyboardHandler,
1453
- useMetricsReporter,
1454
- useNavigation,
1455
- useScrollAnalytics,
1456
- useScrollStore,
1457
- useScrollSystem,
1458
- useTouchHandler,
1459
- useViewControl,
1460
- useViewProgress,
1461
- useViewRegistration,
1462
- useWheelHandler
1849
+ function useScrollLock() {
1850
+ const isLocked = useScrollStore((s) => s.isGlobalLocked);
1851
+ const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1852
+ const setViewExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1853
+ const lock = useCallback(() => {
1854
+ setGlobalLock(true);
1855
+ }, [setGlobalLock]);
1856
+ const unlock = useCallback(() => {
1857
+ setGlobalLock(false);
1858
+ }, [setGlobalLock]);
1859
+ const toggle = useCallback(() => {
1860
+ setGlobalLock(!isLocked);
1861
+ }, [setGlobalLock, isLocked]);
1862
+ const lockView = useCallback((viewId) => {
1863
+ setViewExplicitLock(viewId, "locked");
1864
+ }, [setViewExplicitLock]);
1865
+ const unlockView = useCallback((viewId) => {
1866
+ setViewExplicitLock(viewId, "unlocked");
1867
+ }, [setViewExplicitLock]);
1868
+ return {
1869
+ isLocked,
1870
+ lock,
1871
+ unlock,
1872
+ toggle,
1873
+ lockView,
1874
+ unlockView
1875
+ };
1876
+ }
1877
+ var DEFAULT_CONFIG4 = {
1878
+ speed: 0.5,
1879
+ direction: "vertical",
1880
+ offset: 0,
1881
+ easing: "linear"
1882
+ };
1883
+ var easingFunctions = {
1884
+ linear: (t) => t,
1885
+ easeOut: (t) => 1 - Math.pow(1 - t, 2),
1886
+ easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
1463
1887
  };
1888
+ function useParallax(viewId, config = {}) {
1889
+ const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
1890
+ const { speed, direction, offset, easing } = mergedConfig;
1891
+ const views = useScrollStore((s) => s.views);
1892
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1893
+ useScrollStore((s) => s.globalProgress);
1894
+ const result = useMemo(() => {
1895
+ const view = views.find((v) => v.id === viewId);
1896
+ if (!view) {
1897
+ return {
1898
+ transform: 0,
1899
+ style: {},
1900
+ progress: 0
1901
+ };
1902
+ }
1903
+ const viewOffset = view.index - activeIndex;
1904
+ const internalProgress = view.isActive ? view.progress : 0;
1905
+ const combinedProgress = viewOffset + internalProgress;
1906
+ const easingFn = easingFunctions[easing];
1907
+ const easedProgress = easingFn(Math.abs(combinedProgress)) * Math.sign(combinedProgress);
1908
+ const maxDistance = 100;
1909
+ const transformValue = easedProgress * maxDistance * speed + offset;
1910
+ const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
1911
+ const style = {
1912
+ transform: transformProperty,
1913
+ willChange: "transform"
1914
+ };
1915
+ return {
1916
+ transform: transformValue,
1917
+ style,
1918
+ progress: combinedProgress
1919
+ };
1920
+ }, [viewId, views, activeIndex, speed, direction, offset, easing]);
1921
+ return result;
1922
+ }
1923
+ function useActiveParallax(config = {}) {
1924
+ const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
1925
+ const { speed, direction, offset, easing } = mergedConfig;
1926
+ const globalProgress = useScrollStore((s) => s.globalProgress);
1927
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1928
+ const views = useScrollStore((s) => s.views);
1929
+ return useMemo(() => {
1930
+ const activeView = views[activeIndex];
1931
+ const viewProgress = activeView?.progress ?? 0;
1932
+ const easingFn = easingFunctions[easing];
1933
+ const easedProgress = easingFn(viewProgress);
1934
+ const maxDistance = 100;
1935
+ const transformValue = easedProgress * maxDistance * speed + offset;
1936
+ const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
1937
+ const style = {
1938
+ transform: transformProperty,
1939
+ willChange: "transform"
1940
+ };
1941
+ return {
1942
+ transform: transformValue,
1943
+ style,
1944
+ progress: viewProgress
1945
+ };
1946
+ }, [globalProgress, activeIndex, views, speed, direction, offset, easing]);
1947
+ }
1948
+ function useSnapPoints(options) {
1949
+ const { viewId, points, snapThreshold = 0.1, smoothScroll = true } = options;
1950
+ const scrollContainerRef = useRef(null);
1951
+ const lastActivePointRef = useRef(null);
1952
+ const views = useScrollStore((s) => s.views);
1953
+ const setActiveSnapPoint = useScrollStore((s) => s.setActiveSnapPoint);
1954
+ const view = useMemo(() => views.find((v) => v.id === viewId), [views, viewId]);
1955
+ const viewProgress = view?.progress ?? 0;
1956
+ view?.activeSnapPointId ?? null;
1957
+ const sortedPoints = useMemo(
1958
+ () => [...points].sort((a, b) => a.position - b.position),
1959
+ [points]
1960
+ );
1961
+ const activePointData = useMemo(() => {
1962
+ if (sortedPoints.length === 0) {
1963
+ return { point: null, index: -1 };
1964
+ }
1965
+ let closestIndex = 0;
1966
+ let closestDistance = Math.abs(viewProgress - sortedPoints[0].position);
1967
+ for (let i = 1; i < sortedPoints.length; i++) {
1968
+ const distance = Math.abs(viewProgress - sortedPoints[i].position);
1969
+ if (distance < closestDistance) {
1970
+ closestDistance = distance;
1971
+ closestIndex = i;
1972
+ }
1973
+ }
1974
+ if (closestDistance <= snapThreshold) {
1975
+ return { point: sortedPoints[closestIndex], index: closestIndex };
1976
+ }
1977
+ for (let i = sortedPoints.length - 1; i >= 0; i--) {
1978
+ if (viewProgress >= sortedPoints[i].position - snapThreshold) {
1979
+ return { point: sortedPoints[i], index: i };
1980
+ }
1981
+ }
1982
+ return { point: sortedPoints[0], index: 0 };
1983
+ }, [viewProgress, sortedPoints, snapThreshold]);
1984
+ useEffect(() => {
1985
+ const newPointId = activePointData.point?.id ?? null;
1986
+ if (newPointId !== lastActivePointRef.current) {
1987
+ if (lastActivePointRef.current) {
1988
+ const prevPoint = sortedPoints.find((p) => p.id === lastActivePointRef.current);
1989
+ prevPoint?.onLeave?.();
1990
+ }
1991
+ setActiveSnapPoint(viewId, newPointId);
1992
+ if (newPointId) {
1993
+ const newPoint = sortedPoints.find((p) => p.id === newPointId);
1994
+ newPoint?.onReach?.();
1995
+ }
1996
+ lastActivePointRef.current = newPointId;
1997
+ }
1998
+ }, [activePointData.point?.id, viewId, sortedPoints, setActiveSnapPoint]);
1999
+ const goToPoint = useCallback((pointId) => {
2000
+ const point = sortedPoints.find((p) => p.id === pointId);
2001
+ if (!point || !scrollContainerRef.current) return;
2002
+ const container = scrollContainerRef.current;
2003
+ const maxScroll = container.scrollHeight - container.clientHeight;
2004
+ const targetScroll = point.position * maxScroll;
2005
+ container.scrollTo({
2006
+ top: targetScroll,
2007
+ behavior: smoothScroll ? "smooth" : "auto"
2008
+ });
2009
+ }, [sortedPoints, smoothScroll]);
2010
+ const goToNextPoint = useCallback(() => {
2011
+ const nextIndex = Math.min(activePointData.index + 1, sortedPoints.length - 1);
2012
+ const nextPoint = sortedPoints[nextIndex];
2013
+ if (nextPoint) {
2014
+ goToPoint(nextPoint.id);
2015
+ }
2016
+ }, [activePointData.index, sortedPoints, goToPoint]);
2017
+ const goToPrevPoint = useCallback(() => {
2018
+ const prevIndex = Math.max(activePointData.index - 1, 0);
2019
+ const prevPoint = sortedPoints[prevIndex];
2020
+ if (prevPoint) {
2021
+ goToPoint(prevPoint.id);
2022
+ }
2023
+ }, [activePointData.index, sortedPoints, goToPoint]);
2024
+ const pointsWithState = useMemo(
2025
+ () => sortedPoints.map((point) => ({
2026
+ ...point,
2027
+ isActive: point.id === activePointData.point?.id
2028
+ })),
2029
+ [sortedPoints, activePointData.point?.id]
2030
+ );
2031
+ return {
2032
+ activePoint: activePointData.point,
2033
+ activeIndex: activePointData.index,
2034
+ goToPoint,
2035
+ goToNextPoint,
2036
+ goToPrevPoint,
2037
+ points: pointsWithState,
2038
+ progress: viewProgress
2039
+ };
2040
+ }
2041
+ function createSnapPoints(positions, options) {
2042
+ const { prefix = "snap", labels } = options ?? {};
2043
+ return positions.map((position, index) => ({
2044
+ id: `${prefix}-${index}`,
2045
+ position,
2046
+ label: labels?.[index] ?? `Section ${index + 1}`
2047
+ }));
2048
+ }
2049
+
2050
+ export { AriaLiveRegion, ControlledView, DEFAULT_PROGRESS_DEBOUNCE, DEFAULT_TRANSITION_DURATION, DEFAULT_TRANSITION_EASING, FullView, LazyView, NAVIGATION_COOLDOWN, NAV_THRESHOLDS, NestedScrollItem, NestedScrollView, ScrollContainer, ScrollDebugOverlay, ScrollLockedView, createSnapPoints, selectActiveView, selectActiveViewProgress, selectCanNavigateNext, selectCanNavigatePrevious, selectGlobalProgress, selectIsAutoScrolling, useActiveParallax, useActiveViewProgress, useAutoScroll, useDragHandler, useFocusManagement, useGestureConfig, useGlobalProgress, useHashSync, useInfiniteScroll, useKeyboardHandler, useMetricsReporter, useNavigation, useParallax, usePreload, useScrollAnalytics, useScrollLock, useScrollStore, useScrollSystem, useSnapPoints, useTouchHandler, useViewControl, useViewProgress, useViewRegistration, useWheelHandler };