scroll-system 1.0.1 → 1.1.1

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 +878 -284
  67. package/dist/index.mjs +817 -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();
@@ -263,6 +322,24 @@ function useWheelHandler() {
263
322
  return;
264
323
  }
265
324
  const normalized = normalizeWheel(event);
325
+ const activeView = state.views[state.activeIndex];
326
+ if (activeView?.capability === "internal") {
327
+ const scrollContainer = document.querySelector(
328
+ `[data-view-type="scroll-locked"][data-active="true"] > div`
329
+ );
330
+ if (scrollContainer) {
331
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
332
+ const maxScroll = scrollHeight - clientHeight;
333
+ const isScrollingDown = normalized.pixelY > 0;
334
+ const isScrollingUp = normalized.pixelY < 0;
335
+ const isAtBottom = scrollTop >= maxScroll - 1;
336
+ const isAtTop = scrollTop <= 1;
337
+ if (isScrollingDown && !isAtBottom || isScrollingUp && !isAtTop) {
338
+ scrollAccumulator.current = 0;
339
+ return;
340
+ }
341
+ }
342
+ }
266
343
  const delta = normalized.pixelY;
267
344
  scrollAccumulator.current += delta;
268
345
  const now = Date.now();
@@ -282,7 +359,6 @@ function useWheelHandler() {
282
359
  if (handled) {
283
360
  scrollAccumulator.current = 0;
284
361
  event.preventDefault();
285
- } else {
286
362
  }
287
363
  }
288
364
  };
@@ -290,14 +366,11 @@ function useWheelHandler() {
290
366
  return () => window.removeEventListener("wheel", handleWheel);
291
367
  }, []);
292
368
  }
293
-
294
- // hooks/useTouchHandler.ts
295
- import { useRef as useRef2, useEffect as useEffect2 } from "react";
296
369
  function useTouchHandler(options = {}) {
297
370
  const { enabled = true } = options;
298
- const touchStart = useRef2(null);
299
- const touchStartTime = useRef2(0);
300
- useEffect2(() => {
371
+ const touchStart = useRef(null);
372
+ const touchStartTime = useRef(0);
373
+ useEffect(() => {
301
374
  if (!enabled) return;
302
375
  const handleTouchStart = (e) => {
303
376
  touchStart.current = {
@@ -338,12 +411,9 @@ function useTouchHandler(options = {}) {
338
411
  };
339
412
  }, [enabled]);
340
413
  }
341
-
342
- // hooks/useKeyboardHandler.ts
343
- import { useEffect as useEffect3 } from "react";
344
414
  function useKeyboardHandler(options = {}) {
345
415
  const { enabled = true, preventDefault = true } = options;
346
- useEffect3(() => {
416
+ useEffect(() => {
347
417
  if (!enabled) return;
348
418
  const handleKeyDown = (e) => {
349
419
  const target = e.target;
@@ -401,13 +471,10 @@ function useKeyboardHandler(options = {}) {
401
471
  };
402
472
  }, [enabled, preventDefault]);
403
473
  }
404
-
405
- // hooks/useHashSync.ts
406
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
407
474
  function useHashSync(options = {}) {
408
475
  const { enabled = true, pushHistory = false, hashPrefix = "" } = options;
409
- const hasInitialized = useRef3(false);
410
- useEffect4(() => {
476
+ const hasInitialized = useRef(false);
477
+ useEffect(() => {
411
478
  if (!enabled) return;
412
479
  const unsubscribe = useScrollStore.subscribe(
413
480
  (state) => state.activeIndex,
@@ -428,7 +495,7 @@ function useHashSync(options = {}) {
428
495
  );
429
496
  return () => unsubscribe();
430
497
  }, [enabled, pushHistory, hashPrefix]);
431
- useEffect4(() => {
498
+ useEffect(() => {
432
499
  if (!enabled) return;
433
500
  const handlePopState = () => {
434
501
  const hash = window.location.hash.slice(1);
@@ -443,7 +510,7 @@ function useHashSync(options = {}) {
443
510
  window.addEventListener("popstate", handlePopState);
444
511
  return () => window.removeEventListener("popstate", handlePopState);
445
512
  }, [enabled, hashPrefix]);
446
- useEffect4(() => {
513
+ useEffect(() => {
447
514
  if (!enabled) return;
448
515
  const checkAndNavigate = () => {
449
516
  const state = useScrollStore.getState();
@@ -466,9 +533,6 @@ function useHashSync(options = {}) {
466
533
  checkAndNavigate();
467
534
  }, [enabled, hashPrefix]);
468
535
  }
469
-
470
- // hooks/useDragHandler.ts
471
- import { useRef as useRef4, useEffect as useEffect5, useCallback, useState } from "react";
472
536
  var DRAG_THRESHOLD = 50;
473
537
  var VELOCITY_THRESHOLD = 0.5;
474
538
  var RESISTANCE_FACTOR = 0.4;
@@ -479,9 +543,9 @@ function useDragHandler(options = {}) {
479
543
  dragOffset: 0,
480
544
  dragDirection: null
481
545
  });
482
- const touchStartRef = useRef4(null);
483
- const lastMoveRef = useRef4(null);
484
- const rafRef = useRef4(null);
546
+ const touchStartRef = useRef(null);
547
+ const lastMoveRef = useRef(null);
548
+ const rafRef = useRef(null);
485
549
  const updateDragState = useCallback((newState) => {
486
550
  setDragState((prev) => {
487
551
  const updated = { ...prev, ...newState };
@@ -489,7 +553,7 @@ function useDragHandler(options = {}) {
489
553
  return updated;
490
554
  });
491
555
  }, [onDragUpdate]);
492
- useEffect5(() => {
556
+ useEffect(() => {
493
557
  if (!enabled) return;
494
558
  const handleTouchStart = (e) => {
495
559
  const target = e.target;
@@ -575,16 +639,13 @@ function useDragHandler(options = {}) {
575
639
  }, [enabled, updateDragState, onDragEnd]);
576
640
  return dragState;
577
641
  }
578
-
579
- // hooks/useFocusManagement.ts
580
- import { useEffect as useEffect6, useRef as useRef5 } from "react";
581
642
  function useFocusManagement(options = {}) {
582
643
  const { enabled = true, focusDelay = 100 } = options;
583
644
  const activeIndex = useScrollStore((s) => s.activeIndex);
584
645
  const activeId = useScrollStore((s) => s.activeId);
585
646
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
586
- const prevIndexRef = useRef5(activeIndex);
587
- useEffect6(() => {
647
+ const prevIndexRef = useRef(activeIndex);
648
+ useEffect(() => {
588
649
  if (!enabled) return;
589
650
  if (activeIndex !== prevIndexRef.current && !isTransitioning && activeId) {
590
651
  const timer = setTimeout(() => {
@@ -601,9 +662,6 @@ function useFocusManagement(options = {}) {
601
662
  }
602
663
  }, [activeIndex, activeId, isTransitioning, enabled, focusDelay]);
603
664
  }
604
-
605
- // hooks/useScrollSystem.ts
606
- import { useCallback as useCallback2, useMemo } from "react";
607
665
  function useScrollSystem() {
608
666
  const activeIndex = useScrollStore((s) => s.activeIndex);
609
667
  const globalProgress = useScrollStore((s) => s.globalProgress);
@@ -623,24 +681,24 @@ function useScrollSystem() {
623
681
  const activeViewProgress = activeView?.progress ?? 0;
624
682
  const canNavigateNext = useScrollStore(selectCanNavigateNext);
625
683
  const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
626
- const goToNext = useCallback2(() => {
684
+ const goToNext = useCallback(() => {
627
685
  if (canNavigateNext) {
628
686
  storeNext();
629
687
  return true;
630
688
  }
631
689
  return false;
632
690
  }, [canNavigateNext, storeNext]);
633
- const goToPrev = useCallback2(() => {
691
+ const goToPrev = useCallback(() => {
634
692
  if (canNavigatePrevious) {
635
693
  storePrev();
636
694
  return true;
637
695
  }
638
696
  return false;
639
697
  }, [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]);
698
+ const getCurrentIndex = useCallback(() => activeIndex, [activeIndex]);
699
+ const getProgress = useCallback(() => globalProgress, [globalProgress]);
700
+ const getActiveViewProgress = useCallback(() => activeViewProgress, [activeViewProgress]);
701
+ const isLocked = useCallback(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
644
702
  return useMemo(() => ({
645
703
  goToNext,
646
704
  goToPrev,
@@ -677,6 +735,255 @@ function useScrollSystem() {
677
735
  isTransitioning
678
736
  ]);
679
737
  }
738
+ function useGlobalProgress(options = {}) {
739
+ const { onProgress, throttle: throttle2 = 16 } = options;
740
+ const progress = useScrollStore(selectGlobalProgress);
741
+ const activeIndex = useScrollStore((s) => s.activeIndex);
742
+ const totalViews = useScrollStore((s) => s.totalViews);
743
+ useEffect(() => {
744
+ if (!onProgress) return;
745
+ let lastCall = 0;
746
+ const now = Date.now();
747
+ if (now - lastCall >= throttle2) {
748
+ lastCall = now;
749
+ onProgress(progress);
750
+ }
751
+ }, [progress, onProgress, throttle2]);
752
+ return {
753
+ progress,
754
+ activeIndex,
755
+ totalViews,
756
+ percentage: Math.round(progress * 100)
757
+ };
758
+ }
759
+ var DEFAULT_CONFIG = {
760
+ enabled: false,
761
+ interval: 5e3,
762
+ pauseOnInteraction: true,
763
+ resumeDelay: 3e3,
764
+ direction: "forward",
765
+ stopAtEnd: false
766
+ };
767
+ function useAutoScroll(config) {
768
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
769
+ const {
770
+ enabled,
771
+ interval,
772
+ pauseOnInteraction,
773
+ resumeDelay,
774
+ direction,
775
+ stopAtEnd
776
+ } = mergedConfig;
777
+ const intervalRef = useRef(null);
778
+ const resumeTimeoutRef = useRef(null);
779
+ const isAutoScrolling = useScrollStore((s) => s.isAutoScrolling);
780
+ const isAutoScrollPaused = useScrollStore((s) => s.isAutoScrollPaused);
781
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
782
+ const activeIndex = useScrollStore((s) => s.activeIndex);
783
+ const totalViews = useScrollStore((s) => s.totalViews);
784
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
785
+ const isDragging = useScrollStore((s) => s.isDragging);
786
+ const setAutoScrolling = useScrollStore((s) => s.setAutoScrolling);
787
+ const setAutoScrollPaused = useScrollStore((s) => s.setAutoScrollPaused);
788
+ const goToNext = useScrollStore((s) => s.goToNext);
789
+ const goToPrevious = useScrollStore((s) => s.goToPrevious);
790
+ const pause = useCallback(() => {
791
+ setAutoScrollPaused(true);
792
+ if (resumeTimeoutRef.current) {
793
+ clearTimeout(resumeTimeoutRef.current);
794
+ resumeTimeoutRef.current = null;
795
+ }
796
+ }, [setAutoScrollPaused]);
797
+ const resume = useCallback(() => {
798
+ setAutoScrollPaused(false);
799
+ }, [setAutoScrollPaused]);
800
+ const toggle = useCallback(() => {
801
+ if (isAutoScrollPaused) {
802
+ resume();
803
+ } else {
804
+ pause();
805
+ }
806
+ }, [isAutoScrollPaused, pause, resume]);
807
+ const reset = useCallback(() => {
808
+ if (intervalRef.current) {
809
+ clearInterval(intervalRef.current);
810
+ intervalRef.current = null;
811
+ }
812
+ }, []);
813
+ useEffect(() => {
814
+ if (!enabled || !pauseOnInteraction) return;
815
+ if (isDragging || isTransitioning) {
816
+ pause();
817
+ if (resumeTimeoutRef.current) {
818
+ clearTimeout(resumeTimeoutRef.current);
819
+ }
820
+ resumeTimeoutRef.current = setTimeout(() => {
821
+ if (enabled) {
822
+ resume();
823
+ }
824
+ }, resumeDelay);
825
+ }
826
+ return () => {
827
+ if (resumeTimeoutRef.current) {
828
+ clearTimeout(resumeTimeoutRef.current);
829
+ }
830
+ };
831
+ }, [isDragging, isTransitioning, enabled, pauseOnInteraction, resumeDelay, pause, resume]);
832
+ useEffect(() => {
833
+ setAutoScrolling(enabled);
834
+ setAutoScrollPaused(false);
835
+ return () => {
836
+ setAutoScrolling(false);
837
+ };
838
+ }, [enabled, setAutoScrolling, setAutoScrollPaused]);
839
+ useEffect(() => {
840
+ if (!enabled || isAutoScrollPaused || isTransitioning) {
841
+ reset();
842
+ return;
843
+ }
844
+ if (stopAtEnd && !infiniteScrollEnabled) {
845
+ if (direction === "forward" && activeIndex >= totalViews - 1) {
846
+ return;
847
+ }
848
+ if (direction === "backward" && activeIndex <= 0) {
849
+ return;
850
+ }
851
+ }
852
+ intervalRef.current = setInterval(() => {
853
+ if (direction === "forward") {
854
+ goToNext();
855
+ } else {
856
+ goToPrevious();
857
+ }
858
+ }, interval);
859
+ return () => {
860
+ reset();
861
+ };
862
+ }, [
863
+ enabled,
864
+ isAutoScrollPaused,
865
+ isTransitioning,
866
+ interval,
867
+ direction,
868
+ stopAtEnd,
869
+ activeIndex,
870
+ totalViews,
871
+ infiniteScrollEnabled,
872
+ goToNext,
873
+ goToPrevious,
874
+ reset
875
+ ]);
876
+ return {
877
+ isPlaying: isAutoScrolling && !isAutoScrollPaused,
878
+ isPaused: isAutoScrollPaused,
879
+ pause,
880
+ resume,
881
+ toggle,
882
+ reset
883
+ };
884
+ }
885
+ var DEFAULT_CONFIG2 = {
886
+ enabled: false,
887
+ loopDirection: "both"
888
+ };
889
+ function useInfiniteScroll(config = false) {
890
+ const normalizedConfig = typeof config === "boolean" ? { ...DEFAULT_CONFIG2, enabled: config } : { ...DEFAULT_CONFIG2, ...config };
891
+ const { enabled, loopDirection } = normalizedConfig;
892
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
893
+ const setInfiniteScrollEnabled = useScrollStore((s) => s.setInfiniteScrollEnabled);
894
+ const activeIndex = useScrollStore((s) => s.activeIndex);
895
+ const totalViews = useScrollStore((s) => s.totalViews);
896
+ useEffect(() => {
897
+ setInfiniteScrollEnabled(enabled);
898
+ return () => {
899
+ };
900
+ }, [enabled, setInfiniteScrollEnabled]);
901
+ const enable = () => setInfiniteScrollEnabled(true);
902
+ const disable = () => setInfiniteScrollEnabled(false);
903
+ const toggle = () => setInfiniteScrollEnabled(!infiniteScrollEnabled);
904
+ const canLoopForward = infiniteScrollEnabled && (loopDirection === "forward" || loopDirection === "both") && activeIndex === totalViews - 1;
905
+ const canLoopBackward = infiniteScrollEnabled && (loopDirection === "backward" || loopDirection === "both") && activeIndex === 0;
906
+ return {
907
+ isEnabled: infiniteScrollEnabled,
908
+ enable,
909
+ disable,
910
+ toggle,
911
+ canLoopForward,
912
+ canLoopBackward
913
+ };
914
+ }
915
+ var DEFAULT_CONFIG3 = {
916
+ ahead: 1,
917
+ behind: 1,
918
+ delay: 100
919
+ };
920
+ function usePreload(config = {}) {
921
+ const mergedConfig = { ...DEFAULT_CONFIG3, ...config };
922
+ const { ahead, behind, delay } = mergedConfig;
923
+ const views = useScrollStore((s) => s.views);
924
+ const activeIndex = useScrollStore((s) => s.activeIndex);
925
+ const totalViews = useScrollStore((s) => s.totalViews);
926
+ const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
927
+ const setViewPreloaded = useScrollStore((s) => s.setViewPreloaded);
928
+ const preloadedViewIds = useMemo(() => {
929
+ const ids = [];
930
+ for (let i = -behind; i <= ahead; i++) {
931
+ let targetIndex = activeIndex + i;
932
+ if (infiniteScrollEnabled) {
933
+ if (targetIndex < 0) targetIndex = totalViews + targetIndex;
934
+ if (targetIndex >= totalViews) targetIndex = targetIndex - totalViews;
935
+ }
936
+ if (targetIndex >= 0 && targetIndex < totalViews) {
937
+ const view = views[targetIndex];
938
+ if (view) {
939
+ ids.push(view.id);
940
+ }
941
+ }
942
+ }
943
+ return ids;
944
+ }, [activeIndex, ahead, behind, views, totalViews, infiniteScrollEnabled]);
945
+ useEffect(() => {
946
+ const timer = setTimeout(() => {
947
+ views.forEach((view) => {
948
+ const shouldBePreloaded = preloadedViewIds.includes(view.id);
949
+ if (view.isPreloaded !== shouldBePreloaded) {
950
+ setViewPreloaded(view.id, shouldBePreloaded);
951
+ }
952
+ });
953
+ }, delay);
954
+ return () => clearTimeout(timer);
955
+ }, [preloadedViewIds, views, setViewPreloaded, delay]);
956
+ const shouldPreload = (viewId) => {
957
+ return preloadedViewIds.includes(viewId);
958
+ };
959
+ const isPreloaded = (viewId) => {
960
+ const view = views.find((v) => v.id === viewId);
961
+ return view?.isPreloaded ?? false;
962
+ };
963
+ return {
964
+ shouldPreload,
965
+ preloadedViewIds,
966
+ isPreloaded
967
+ };
968
+ }
969
+ var DEFAULT_GESTURE_CONFIG = {
970
+ swipeThreshold: 50,
971
+ swipeVelocity: 0.5,
972
+ dragResistance: 0.3,
973
+ enableWheel: true,
974
+ enableTouch: true,
975
+ enableKeyboard: true
976
+ };
977
+ var GestureConfigContext = createContext(DEFAULT_GESTURE_CONFIG);
978
+ function useGestureConfig() {
979
+ return useContext(GestureConfigContext);
980
+ }
981
+ function mergeGestureConfig(config) {
982
+ return useMemo(() => ({
983
+ ...DEFAULT_GESTURE_CONFIG,
984
+ ...config
985
+ }), [config]);
986
+ }
680
987
 
681
988
  // utils/index.ts
682
989
  function throttle(fn, limit) {
@@ -705,9 +1012,6 @@ function prefersReducedMotion() {
705
1012
  if (typeof window === "undefined") return false;
706
1013
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
707
1014
  }
708
-
709
- // components/ScrollContainer.tsx
710
- import { jsx } from "react/jsx-runtime";
711
1015
  function ScrollContainer({
712
1016
  children,
713
1017
  className = "",
@@ -725,12 +1029,20 @@ function ScrollContainer({
725
1029
  // Touch Physics
726
1030
  enableDragPhysics = false,
727
1031
  // Layout
728
- orientation = "vertical"
1032
+ orientation = "vertical",
1033
+ // NEW: v1.1.0 Features
1034
+ skipInitialAnimation = false,
1035
+ onProgress,
1036
+ gestureConfig,
1037
+ autoScroll,
1038
+ infiniteScroll = false,
1039
+ preload = true
729
1040
  }) {
730
- const containerRef = useRef6(null);
1041
+ const containerRef = useRef(null);
1042
+ const isFirstRender = useRef(true);
731
1043
  const isBrowser = typeof window !== "undefined";
732
- const [reducedMotion, setReducedMotion] = useState2(false);
733
- useEffect7(() => {
1044
+ const [reducedMotion, setReducedMotion] = useState(false);
1045
+ useEffect(() => {
734
1046
  if (!isBrowser || !respectReducedMotion) return;
735
1047
  setReducedMotion(prefersReducedMotion());
736
1048
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
@@ -741,9 +1053,9 @@ function ScrollContainer({
741
1053
  const effectiveDuration = reducedMotion ? 0 : transitionDuration;
742
1054
  const { initialize, endTransition } = useScrollStore();
743
1055
  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);
1056
+ useScrollStore((s) => s.totalViews);
1057
+ useScrollStore((s) => s.isInitialized);
1058
+ const prevIndexRef = useRef(activeIndex);
747
1059
  useWheelHandler();
748
1060
  useTouchHandler({ enabled: !enableDragPhysics });
749
1061
  useKeyboardHandler();
@@ -756,14 +1068,39 @@ function ScrollContainer({
756
1068
  hashPrefix
757
1069
  });
758
1070
  useFocusManagement({ enabled: enableFocusManagement });
759
- useEffect7(() => {
1071
+ useGlobalProgress({ onProgress });
1072
+ useInfiniteScroll(infiniteScroll);
1073
+ const autoScrollState = useAutoScroll(
1074
+ autoScroll ?? { enabled: false, interval: 5e3 }
1075
+ );
1076
+ const preloadConfig = typeof preload === "boolean" ? preload ? { ahead: 1, behind: 1 } : void 0 : preload;
1077
+ usePreload(preloadConfig ?? {});
1078
+ const mergedGestureConfig = mergeGestureConfig(gestureConfig);
1079
+ useEffect(() => {
760
1080
  const timer = setTimeout(() => {
761
1081
  initialize();
762
1082
  onInitialized?.();
1083
+ isFirstRender.current = false;
763
1084
  }, 50);
764
1085
  return () => clearTimeout(timer);
765
1086
  }, [initialize, onInitialized]);
766
- useEffect7(() => {
1087
+ useEffect(() => {
1088
+ if (!isBrowser) return;
1089
+ const html = document.documentElement;
1090
+ const body = document.body;
1091
+ const originalHtmlOverscroll = html.style.overscrollBehavior;
1092
+ const originalBodyOverscroll = body.style.overscrollBehavior;
1093
+ const originalTouchAction = body.style.touchAction;
1094
+ html.style.overscrollBehavior = "none";
1095
+ body.style.overscrollBehavior = "none";
1096
+ body.style.touchAction = "pan-x pan-y";
1097
+ return () => {
1098
+ html.style.overscrollBehavior = originalHtmlOverscroll;
1099
+ body.style.overscrollBehavior = originalBodyOverscroll;
1100
+ body.style.touchAction = originalTouchAction;
1101
+ };
1102
+ }, [isBrowser]);
1103
+ useEffect(() => {
767
1104
  if (prevIndexRef.current !== activeIndex) {
768
1105
  onViewChange?.(prevIndexRef.current, activeIndex);
769
1106
  const timer = setTimeout(() => {
@@ -773,37 +1110,34 @@ function ScrollContainer({
773
1110
  return () => clearTimeout(timer);
774
1111
  }
775
1112
  }, [activeIndex, effectiveDuration, onViewChange, endTransition]);
776
- const wrapperStyle = useMemo2(() => {
1113
+ const wrapperStyle = useMemo(() => {
777
1114
  const baseOffset = activeIndex * 100;
778
1115
  const dragOffset = dragState.isDragging ? dragState.dragOffset * 100 : 0;
779
1116
  const transformAxis = orientation === "horizontal" ? "X" : "Y";
780
1117
  const sizeUnit = orientation === "horizontal" ? "vw" : "vh";
1118
+ const shouldAnimate = !skipInitialAnimation || !isFirstRender.current;
1119
+ const transitionValue = dragState.isDragging ? "none" : shouldAnimate && effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none";
781
1120
  return {
782
1121
  transform: `translate${transformAxis}(-${baseOffset + dragOffset}${sizeUnit})`,
783
- transition: dragState.isDragging ? "none" : effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none",
1122
+ transition: transitionValue,
784
1123
  height: "100%",
785
1124
  width: "100%",
786
1125
  display: orientation === "horizontal" ? "flex" : "block",
787
1126
  flexDirection: orientation === "horizontal" ? "row" : void 0
788
1127
  };
789
- }, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation]);
790
- return /* @__PURE__ */ jsx(
1128
+ }, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation, skipInitialAnimation]);
1129
+ return /* @__PURE__ */ jsx(GestureConfigContext.Provider, { value: mergedGestureConfig, children: /* @__PURE__ */ jsx(
791
1130
  "div",
792
1131
  {
793
1132
  ref: containerRef,
794
1133
  className: `scroll-container fixed inset-0 overflow-hidden w-screen h-screen ${className}`,
795
1134
  role: "main",
796
1135
  "aria-label": "Scroll container",
1136
+ "data-auto-scrolling": autoScrollState.isPlaying,
797
1137
  children: /* @__PURE__ */ jsx("div", { className: "scroll-wrapper", style: wrapperStyle, children })
798
1138
  }
799
- );
1139
+ ) });
800
1140
  }
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
1141
  function useViewRegistration({
808
1142
  config,
809
1143
  onActivate,
@@ -817,7 +1151,7 @@ function useViewRegistration({
817
1151
  const unregisterView = useScrollStore((s) => s.unregisterView);
818
1152
  const activeId = useScrollStore((s) => s.activeId);
819
1153
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
820
- const callbacksRef = useRef7({
1154
+ const callbacksRef = useRef({
821
1155
  onActivate,
822
1156
  onDeactivate,
823
1157
  onEnterStart,
@@ -833,20 +1167,20 @@ function useViewRegistration({
833
1167
  onExitStart,
834
1168
  onExitEnd
835
1169
  };
836
- const wasActiveRef = useRef7(false);
837
- const wasTransitioningRef = useRef7(false);
838
- const configRef = useRef7(config);
1170
+ const wasActiveRef = useRef(false);
1171
+ const wasTransitioningRef = useRef(false);
1172
+ const configRef = useRef(config);
839
1173
  if (JSON.stringify(config) !== JSON.stringify(configRef.current)) {
840
1174
  configRef.current = config;
841
1175
  }
842
- useEffect8(() => {
1176
+ useEffect(() => {
843
1177
  const currentConfig = configRef.current;
844
1178
  registerView(currentConfig);
845
1179
  return () => unregisterView(currentConfig.id);
846
1180
  }, [registerView, unregisterView, configRef.current]);
847
1181
  const viewState = useScrollStore((s) => s.views.find((v) => v.id === config.id));
848
1182
  const isActive = activeId === config.id;
849
- useEffect8(() => {
1183
+ useEffect(() => {
850
1184
  if (isActive && !wasActiveRef.current) {
851
1185
  callbacksRef.current.onActivate?.();
852
1186
  } else if (!isActive && wasActiveRef.current) {
@@ -854,7 +1188,7 @@ function useViewRegistration({
854
1188
  }
855
1189
  wasActiveRef.current = isActive;
856
1190
  }, [isActive]);
857
- useEffect8(() => {
1191
+ useEffect(() => {
858
1192
  const callbacks = callbacksRef.current;
859
1193
  const wasActive = wasActiveRef.current;
860
1194
  const wasTransitioning = wasTransitioningRef.current;
@@ -882,9 +1216,6 @@ function useViewRegistration({
882
1216
  navigation: viewState?.navigation ?? "unlocked"
883
1217
  };
884
1218
  }
885
-
886
- // components/FullView.tsx
887
- import { jsx as jsx2 } from "react/jsx-runtime";
888
1219
  function FullView({
889
1220
  id,
890
1221
  children,
@@ -897,7 +1228,7 @@ function FullView({
897
1228
  onExitStart,
898
1229
  onExitEnd
899
1230
  }) {
900
- const config = useMemo3(
1231
+ const config = useMemo(
901
1232
  () => ({
902
1233
  id,
903
1234
  type: "full",
@@ -914,7 +1245,7 @@ function FullView({
914
1245
  onExitStart,
915
1246
  onExitEnd
916
1247
  });
917
- return /* @__PURE__ */ jsx2(
1248
+ return /* @__PURE__ */ jsx(
918
1249
  "section",
919
1250
  {
920
1251
  id,
@@ -934,9 +1265,6 @@ function FullView({
934
1265
  }
935
1266
  );
936
1267
  }
937
-
938
- // hooks/useMetricsReporter.ts
939
- import { useRef as useRef8, useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4 } from "react";
940
1268
  var SCROLL_THROTTLE_MS = 66;
941
1269
  function useMetricsReporter({
942
1270
  id,
@@ -945,9 +1273,9 @@ function useMetricsReporter({
945
1273
  onScrollProgress,
946
1274
  throttleMs = SCROLL_THROTTLE_MS
947
1275
  }) {
948
- const scrollRef = useRef8(null);
1276
+ const scrollRef = useRef(null);
949
1277
  const updateMetrics = useScrollStore((s) => s.updateViewMetrics);
950
- const measureAndReport = useCallback3(() => {
1278
+ const measureAndReport = useCallback(() => {
951
1279
  const el = scrollRef.current;
952
1280
  if (!el) return;
953
1281
  const metrics = {
@@ -962,11 +1290,11 @@ function useMetricsReporter({
962
1290
  onScrollProgress(progress);
963
1291
  }
964
1292
  }, [id, scrollDirection, updateMetrics, onScrollProgress]);
965
- const throttledMeasure = useMemo4(
1293
+ const throttledMeasure = useMemo(
966
1294
  () => throttle(measureAndReport, throttleMs),
967
1295
  [measureAndReport, throttleMs]
968
1296
  );
969
- useEffect9(() => {
1297
+ useEffect(() => {
970
1298
  const el = scrollRef.current;
971
1299
  if (!el) return;
972
1300
  const resizeObserver = new ResizeObserver(() => {
@@ -981,16 +1309,13 @@ function useMetricsReporter({
981
1309
  el.removeEventListener("scroll", throttledMeasure);
982
1310
  };
983
1311
  }, [measureAndReport, throttledMeasure]);
984
- useEffect9(() => {
1312
+ useEffect(() => {
985
1313
  if (isActive) {
986
1314
  measureAndReport();
987
1315
  }
988
1316
  }, [isActive, measureAndReport]);
989
1317
  return { scrollRef, measureAndReport };
990
1318
  }
991
-
992
- // components/ScrollLockedView.tsx
993
- import { jsx as jsx3 } from "react/jsx-runtime";
994
1319
  function ScrollLockedView({
995
1320
  id,
996
1321
  children,
@@ -1026,14 +1351,14 @@ function ScrollLockedView({
1026
1351
  onScrollProgress
1027
1352
  });
1028
1353
  const scrollClasses = scrollDirection === "vertical" ? "overflow-y-auto overflow-x-hidden" : "overflow-x-auto overflow-y-hidden";
1029
- return /* @__PURE__ */ jsx3(
1354
+ return /* @__PURE__ */ jsx(
1030
1355
  "section",
1031
1356
  {
1032
1357
  id,
1033
1358
  className: `relative w-full h-screen ${className}`,
1034
1359
  "data-view-type": "scroll-locked",
1035
1360
  "data-active": isActive,
1036
- children: /* @__PURE__ */ jsx3(
1361
+ children: /* @__PURE__ */ jsx(
1037
1362
  "div",
1038
1363
  {
1039
1364
  ref: scrollRef,
@@ -1045,10 +1370,6 @@ function ScrollLockedView({
1045
1370
  }
1046
1371
  );
1047
1372
  }
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
1373
  function ControlledView({
1053
1374
  id,
1054
1375
  children,
@@ -1065,7 +1386,7 @@ function ControlledView({
1065
1386
  onExitEnd
1066
1387
  }) {
1067
1388
  const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1068
- const config = useMemo5(
1389
+ const config = useMemo(
1069
1390
  () => ({
1070
1391
  id,
1071
1392
  type: "controlled",
@@ -1089,11 +1410,11 @@ function ControlledView({
1089
1410
  isActive,
1090
1411
  scrollDirection: allowInternalScroll ? scrollDirection : "none"
1091
1412
  });
1092
- useEffect10(() => {
1413
+ useEffect(() => {
1093
1414
  const lockState = canProceed ? "unlocked" : "locked";
1094
1415
  setExplicitLock(id, lockState);
1095
1416
  }, [id, canProceed, setExplicitLock]);
1096
- const overflowClasses = useMemo5(() => {
1417
+ const overflowClasses = useMemo(() => {
1097
1418
  if (!allowInternalScroll) return "overflow-hidden";
1098
1419
  switch (scrollDirection) {
1099
1420
  case "vertical":
@@ -1104,14 +1425,14 @@ function ControlledView({
1104
1425
  return "overflow-auto";
1105
1426
  }
1106
1427
  }, [allowInternalScroll, scrollDirection]);
1107
- return /* @__PURE__ */ jsx4(
1428
+ return /* @__PURE__ */ jsx(
1108
1429
  "section",
1109
1430
  {
1110
1431
  id,
1111
1432
  className: `relative w-full h-screen ${isActive ? "z-10" : "z-0"} ${className}`,
1112
1433
  "data-view-type": "controlled",
1113
1434
  "data-active": isActive,
1114
- children: /* @__PURE__ */ jsx4(
1435
+ children: /* @__PURE__ */ jsx(
1115
1436
  "div",
1116
1437
  {
1117
1438
  ref: scrollRef,
@@ -1127,7 +1448,7 @@ function useViewControl(viewId) {
1127
1448
  const goToNext = useScrollStore((s) => s.goToNext);
1128
1449
  const goToPrevious = useScrollStore((s) => s.goToPrevious);
1129
1450
  const goToView = useScrollStore((s) => s.goToView);
1130
- return useMemo5(
1451
+ return useMemo(
1131
1452
  () => ({
1132
1453
  unlock: () => setExplicitLock(viewId, "unlocked"),
1133
1454
  lock: () => setExplicitLock(viewId, "locked"),
@@ -1139,9 +1460,6 @@ function useViewControl(viewId) {
1139
1460
  [viewId, setExplicitLock, goToNext, goToPrevious, goToView]
1140
1461
  );
1141
1462
  }
1142
-
1143
- // components/ScrollDebugOverlay.tsx
1144
- import { jsx as jsx5, jsxs } from "react/jsx-runtime";
1145
1463
  function ScrollDebugOverlay({
1146
1464
  position = "bottom-left",
1147
1465
  visible = true
@@ -1167,29 +1485,29 @@ function ScrollDebugOverlay({
1167
1485
  style: { pointerEvents: "none" },
1168
1486
  children: [
1169
1487
  /* @__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" }),
1488
+ /* @__PURE__ */ jsx("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
1171
1489
  "ScrollSystem Debug"
1172
1490
  ] }),
1173
1491
  /* @__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 })
1492
+ /* @__PURE__ */ jsx(Row, { label: "initialized", value: isInitialized }),
1493
+ /* @__PURE__ */ jsx(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
1494
+ /* @__PURE__ */ jsx(Row, { label: "transitioning", value: isTransitioning }),
1495
+ /* @__PURE__ */ jsx(Row, { label: "globalLocked", value: isGlobalLocked })
1178
1496
  ] }),
1179
1497
  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 })
1498
+ /* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
1499
+ /* @__PURE__ */ jsx(Row, { label: "id", value: activeView.id }),
1500
+ /* @__PURE__ */ jsx(Row, { label: "type", value: activeView.type }),
1501
+ /* @__PURE__ */ jsx(Row, { label: "capability", value: activeView.capability }),
1502
+ /* @__PURE__ */ jsx(Row, { label: "navigation", value: activeView.navigation }),
1503
+ /* @__PURE__ */ jsx(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
1504
+ activeView.explicitLock && /* @__PURE__ */ jsx(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
1187
1505
  ] }),
1188
1506
  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) })
1507
+ /* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
1508
+ /* @__PURE__ */ jsx(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
1509
+ /* @__PURE__ */ jsx(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
1510
+ /* @__PURE__ */ jsx(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
1193
1511
  ] })
1194
1512
  ]
1195
1513
  }
@@ -1207,23 +1525,19 @@ function Row({
1207
1525
  label,
1208
1526
  ":"
1209
1527
  ] }),
1210
- /* @__PURE__ */ jsx5("span", { className: valueColor, children: displayValue })
1528
+ /* @__PURE__ */ jsx("span", { className: valueColor, children: displayValue })
1211
1529
  ] });
1212
1530
  }
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
1531
  function AriaLiveRegion({
1218
1532
  template = "Navigated to section {viewIndex} of {totalViews}",
1219
1533
  politeness = "polite"
1220
1534
  }) {
1221
- const [announcement, setAnnouncement] = useState3("");
1535
+ const [announcement, setAnnouncement] = useState("");
1222
1536
  const activeIndex = useScrollStore((s) => s.activeIndex);
1223
1537
  const activeId = useScrollStore((s) => s.activeId);
1224
1538
  const totalViews = useScrollStore((s) => s.totalViews);
1225
1539
  const isTransitioning = useScrollStore((s) => s.isTransitioning);
1226
- useEffect11(() => {
1540
+ useEffect(() => {
1227
1541
  if (!isTransitioning && activeId) {
1228
1542
  const message = template.replace("{viewIndex}", String(activeIndex + 1)).replace("{viewId}", activeId).replace("{totalViews}", String(totalViews));
1229
1543
  const timer = setTimeout(() => {
@@ -1232,7 +1546,7 @@ function AriaLiveRegion({
1232
1546
  return () => clearTimeout(timer);
1233
1547
  }
1234
1548
  }, [activeIndex, activeId, totalViews, isTransitioning, template]);
1235
- return /* @__PURE__ */ jsx6(
1549
+ return /* @__PURE__ */ jsx(
1236
1550
  "div",
1237
1551
  {
1238
1552
  role: "status",
@@ -1254,10 +1568,6 @@ function AriaLiveRegion({
1254
1568
  }
1255
1569
  );
1256
1570
  }
1257
-
1258
- // components/LazyView.tsx
1259
- import { useMemo as useMemo6 } from "react";
1260
- import { Fragment, jsx as jsx7 } from "react/jsx-runtime";
1261
1571
  function LazyView({
1262
1572
  viewId,
1263
1573
  buffer = 1,
@@ -1266,7 +1576,7 @@ function LazyView({
1266
1576
  }) {
1267
1577
  const activeIndex = useScrollStore((s) => s.activeIndex);
1268
1578
  const views = useScrollStore((s) => s.views);
1269
- const shouldRender = useMemo6(() => {
1579
+ const shouldRender = useMemo(() => {
1270
1580
  const viewIndex = views.findIndex((v) => v.id === viewId);
1271
1581
  if (viewIndex === -1) return false;
1272
1582
  const minIndex = Math.max(0, activeIndex - buffer);
@@ -1274,20 +1584,149 @@ function LazyView({
1274
1584
  return viewIndex >= minIndex && viewIndex <= maxIndex;
1275
1585
  }, [viewId, views, activeIndex, buffer]);
1276
1586
  if (!shouldRender) {
1277
- return /* @__PURE__ */ jsx7(Fragment, { children: placeholder });
1587
+ return /* @__PURE__ */ jsx(Fragment, { children: placeholder });
1278
1588
  }
1279
- return /* @__PURE__ */ jsx7(Fragment, { children });
1589
+ return /* @__PURE__ */ jsx(Fragment, { children });
1590
+ }
1591
+ function NestedScrollView({
1592
+ id,
1593
+ children,
1594
+ className = "",
1595
+ nestedDirection = "horizontal",
1596
+ enableSnap = true,
1597
+ onItemChange,
1598
+ onActivate,
1599
+ onDeactivate,
1600
+ onEnterStart,
1601
+ onEnterEnd,
1602
+ onExitStart,
1603
+ onExitEnd
1604
+ }) {
1605
+ const containerRef = useRef(null);
1606
+ const nestedContainerRef = useRef(null);
1607
+ const [activeNestedIndex, setActiveNestedIndex] = useState(0);
1608
+ const [isNestedScrolling, setIsNestedScrolling] = useState(false);
1609
+ useScrollStore((s) => s.activeIndex);
1610
+ const views = useScrollStore((s) => s.views);
1611
+ const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1612
+ const view = views.find((v) => v.id === id);
1613
+ const isActive = view?.isActive ?? false;
1614
+ useViewRegistration({
1615
+ config: {
1616
+ id,
1617
+ type: "nested",
1618
+ nestedConfig: {
1619
+ direction: nestedDirection,
1620
+ enableSnap,
1621
+ onItemChange
1622
+ }
1623
+ },
1624
+ onActivate,
1625
+ onDeactivate,
1626
+ onEnterStart,
1627
+ onEnterEnd,
1628
+ onExitStart,
1629
+ onExitEnd
1630
+ });
1631
+ useEffect(() => {
1632
+ if (isActive) {
1633
+ onActivate?.();
1634
+ } else {
1635
+ onDeactivate?.();
1636
+ }
1637
+ }, [isActive, onActivate, onDeactivate]);
1638
+ const handleNestedScroll = useCallback((e) => {
1639
+ if (!nestedContainerRef.current) return;
1640
+ const container = nestedContainerRef.current;
1641
+ const scrollPos = nestedDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
1642
+ const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
1643
+ if (itemSize > 0) {
1644
+ const newIndex = Math.round(scrollPos / itemSize);
1645
+ if (newIndex !== activeNestedIndex) {
1646
+ setActiveNestedIndex(newIndex);
1647
+ onItemChange?.(newIndex);
1648
+ }
1649
+ }
1650
+ }, [nestedDirection, activeNestedIndex, onItemChange]);
1651
+ const handleTouchStart = useCallback((e) => {
1652
+ setIsNestedScrolling(true);
1653
+ setGlobalLock(true);
1654
+ }, [setGlobalLock]);
1655
+ const handleTouchEnd = useCallback(() => {
1656
+ setTimeout(() => {
1657
+ setIsNestedScrolling(false);
1658
+ setGlobalLock(false);
1659
+ }, 100);
1660
+ }, [setGlobalLock]);
1661
+ useCallback((index) => {
1662
+ if (!nestedContainerRef.current) return;
1663
+ const container = nestedContainerRef.current;
1664
+ const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
1665
+ const scrollPos = index * itemSize;
1666
+ container.scrollTo({
1667
+ [nestedDirection === "horizontal" ? "left" : "top"]: scrollPos,
1668
+ behavior: "smooth"
1669
+ });
1670
+ }, [nestedDirection]);
1671
+ const nestedScrollStyle = {
1672
+ display: "flex",
1673
+ flexDirection: nestedDirection === "horizontal" ? "row" : "column",
1674
+ overflow: nestedDirection === "horizontal" ? "auto hidden" : "hidden auto",
1675
+ scrollSnapType: enableSnap ? `${nestedDirection === "horizontal" ? "x" : "y"} mandatory` : "none",
1676
+ scrollBehavior: "smooth",
1677
+ WebkitOverflowScrolling: "touch",
1678
+ width: "100%",
1679
+ height: "100%"
1680
+ };
1681
+ return /* @__PURE__ */ jsx(
1682
+ "div",
1683
+ {
1684
+ ref: containerRef,
1685
+ className: `scroll-view nested-scroll-view h-screen w-screen flex-shrink-0 relative ${className}`,
1686
+ "data-view-id": id,
1687
+ "data-view-type": "nested",
1688
+ "data-nested-direction": nestedDirection,
1689
+ role: "region",
1690
+ "aria-label": `Nested scroll view ${id}`,
1691
+ tabIndex: 0,
1692
+ children: /* @__PURE__ */ jsx(
1693
+ "div",
1694
+ {
1695
+ ref: nestedContainerRef,
1696
+ className: "nested-scroll-container",
1697
+ style: nestedScrollStyle,
1698
+ onScroll: handleNestedScroll,
1699
+ onTouchStart: handleTouchStart,
1700
+ onTouchEnd: handleTouchEnd,
1701
+ children
1702
+ }
1703
+ )
1704
+ }
1705
+ );
1706
+ }
1707
+ function NestedScrollItem({
1708
+ children,
1709
+ className = ""
1710
+ }) {
1711
+ return /* @__PURE__ */ jsx(
1712
+ "div",
1713
+ {
1714
+ className: `nested-scroll-item flex-shrink-0 w-full h-full ${className}`,
1715
+ style: {
1716
+ scrollSnapAlign: "start",
1717
+ scrollSnapStop: "always"
1718
+ },
1719
+ children
1720
+ }
1721
+ );
1280
1722
  }
1281
-
1282
- // hooks/useNavigation.ts
1283
- import { useCallback as useCallback4, useMemo as useMemo7 } from "react";
1284
1723
  function useNavigation() {
1285
1724
  const goToView = useScrollStore((s) => s.goToView);
1286
1725
  const goToNextAction = useScrollStore((s) => s.goToNext);
1287
1726
  const goToPreviousAction = useScrollStore((s) => s.goToPrevious);
1288
1727
  const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1289
- const lockScroll = useCallback4(() => setGlobalLock(true), [setGlobalLock]);
1290
- const unlockScroll = useCallback4(() => setGlobalLock(false), [setGlobalLock]);
1728
+ const lockScroll = useCallback(() => setGlobalLock(true), [setGlobalLock]);
1729
+ const unlockScroll = useCallback(() => setGlobalLock(false), [setGlobalLock]);
1291
1730
  const activeIndex = useScrollStore((s) => s.activeIndex);
1292
1731
  const activeId = useScrollStore((s) => s.activeId);
1293
1732
  const totalViews = useScrollStore((s) => s.totalViews);
@@ -1295,16 +1734,16 @@ function useNavigation() {
1295
1734
  const isScrollLocked = useScrollStore((s) => s.isGlobalLocked);
1296
1735
  const canNavigateNext = useScrollStore(selectCanNavigateNext);
1297
1736
  const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
1298
- const goToNext = useCallback4(() => {
1737
+ const goToNext = useCallback(() => {
1299
1738
  return goToNextAction();
1300
1739
  }, [goToNextAction]);
1301
- const goToPrevious = useCallback4(() => {
1740
+ const goToPrevious = useCallback(() => {
1302
1741
  return goToPreviousAction();
1303
1742
  }, [goToPreviousAction]);
1304
1743
  const isFirstView = activeIndex === 0;
1305
1744
  const isLastView = activeIndex === totalViews - 1;
1306
1745
  const progress = totalViews > 1 ? activeIndex / (totalViews - 1) : 0;
1307
- return useMemo7(
1746
+ return useMemo(
1308
1747
  () => ({
1309
1748
  // Acciones de navegación
1310
1749
  goToView,
@@ -1374,18 +1813,15 @@ function useActiveViewProgress() {
1374
1813
  viewType: activeView?.type
1375
1814
  };
1376
1815
  }
1377
-
1378
- // hooks/useScrollAnalytics.ts
1379
- import { useEffect as useEffect12, useRef as useRef9, useCallback as useCallback5 } from "react";
1380
1816
  function useScrollAnalytics(options = {}) {
1381
1817
  const { onViewEnter, onViewExit, enabled = true } = options;
1382
1818
  const activeIndex = useScrollStore((s) => s.activeIndex);
1383
1819
  const activeId = useScrollStore((s) => s.activeId);
1384
1820
  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) => ({
1821
+ const enterTimeRef = useRef(Date.now());
1822
+ const prevIndexRef = useRef(activeIndex);
1823
+ const prevIdRef = useRef(activeId);
1824
+ const createAnalytics = useCallback((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
1389
1825
  viewId,
1390
1826
  viewIndex,
1391
1827
  enterTime,
@@ -1393,7 +1829,7 @@ function useScrollAnalytics(options = {}) {
1393
1829
  duration: exitTime ? (exitTime - enterTime) / 1e3 : 0,
1394
1830
  isActive
1395
1831
  }), []);
1396
- useEffect12(() => {
1832
+ useEffect(() => {
1397
1833
  if (!enabled) return;
1398
1834
  if (!isTransitioning && (activeIndex !== prevIndexRef.current || activeId !== prevIdRef.current)) {
1399
1835
  const now = Date.now();
@@ -1428,36 +1864,205 @@ function useScrollAnalytics(options = {}) {
1428
1864
  getTimeInView: () => (Date.now() - enterTimeRef.current) / 1e3
1429
1865
  };
1430
1866
  }
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
1867
+ function useScrollLock() {
1868
+ const isLocked = useScrollStore((s) => s.isGlobalLocked);
1869
+ const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1870
+ const setViewExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1871
+ const lock = useCallback(() => {
1872
+ setGlobalLock(true);
1873
+ }, [setGlobalLock]);
1874
+ const unlock = useCallback(() => {
1875
+ setGlobalLock(false);
1876
+ }, [setGlobalLock]);
1877
+ const toggle = useCallback(() => {
1878
+ setGlobalLock(!isLocked);
1879
+ }, [setGlobalLock, isLocked]);
1880
+ const lockView = useCallback((viewId) => {
1881
+ setViewExplicitLock(viewId, "locked");
1882
+ }, [setViewExplicitLock]);
1883
+ const unlockView = useCallback((viewId) => {
1884
+ setViewExplicitLock(viewId, "unlocked");
1885
+ }, [setViewExplicitLock]);
1886
+ return {
1887
+ isLocked,
1888
+ lock,
1889
+ unlock,
1890
+ toggle,
1891
+ lockView,
1892
+ unlockView
1893
+ };
1894
+ }
1895
+ var DEFAULT_CONFIG4 = {
1896
+ speed: 0.5,
1897
+ direction: "vertical",
1898
+ offset: 0,
1899
+ easing: "linear"
1900
+ };
1901
+ var easingFunctions = {
1902
+ linear: (t) => t,
1903
+ easeOut: (t) => 1 - Math.pow(1 - t, 2),
1904
+ easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
1463
1905
  };
1906
+ function useParallax(viewId, config = {}) {
1907
+ const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
1908
+ const { speed, direction, offset, easing } = mergedConfig;
1909
+ const views = useScrollStore((s) => s.views);
1910
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1911
+ useScrollStore((s) => s.globalProgress);
1912
+ const result = useMemo(() => {
1913
+ const view = views.find((v) => v.id === viewId);
1914
+ if (!view) {
1915
+ return {
1916
+ transform: 0,
1917
+ style: {},
1918
+ progress: 0
1919
+ };
1920
+ }
1921
+ const viewOffset = view.index - activeIndex;
1922
+ const internalProgress = view.isActive ? view.progress : 0;
1923
+ const combinedProgress = viewOffset + internalProgress;
1924
+ const easingFn = easingFunctions[easing];
1925
+ const easedProgress = easingFn(Math.abs(combinedProgress)) * Math.sign(combinedProgress);
1926
+ const maxDistance = 100;
1927
+ const transformValue = easedProgress * maxDistance * speed + offset;
1928
+ const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
1929
+ const style = {
1930
+ transform: transformProperty,
1931
+ willChange: "transform"
1932
+ };
1933
+ return {
1934
+ transform: transformValue,
1935
+ style,
1936
+ progress: combinedProgress
1937
+ };
1938
+ }, [viewId, views, activeIndex, speed, direction, offset, easing]);
1939
+ return result;
1940
+ }
1941
+ function useActiveParallax(config = {}) {
1942
+ const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
1943
+ const { speed, direction, offset, easing } = mergedConfig;
1944
+ const globalProgress = useScrollStore((s) => s.globalProgress);
1945
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1946
+ const views = useScrollStore((s) => s.views);
1947
+ return useMemo(() => {
1948
+ const activeView = views[activeIndex];
1949
+ const viewProgress = activeView?.progress ?? 0;
1950
+ const easingFn = easingFunctions[easing];
1951
+ const easedProgress = easingFn(viewProgress);
1952
+ const maxDistance = 100;
1953
+ const transformValue = easedProgress * maxDistance * speed + offset;
1954
+ const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
1955
+ const style = {
1956
+ transform: transformProperty,
1957
+ willChange: "transform"
1958
+ };
1959
+ return {
1960
+ transform: transformValue,
1961
+ style,
1962
+ progress: viewProgress
1963
+ };
1964
+ }, [globalProgress, activeIndex, views, speed, direction, offset, easing]);
1965
+ }
1966
+ function useSnapPoints(options) {
1967
+ const { viewId, points, snapThreshold = 0.1, smoothScroll = true } = options;
1968
+ const scrollContainerRef = useRef(null);
1969
+ const lastActivePointRef = useRef(null);
1970
+ const views = useScrollStore((s) => s.views);
1971
+ const setActiveSnapPoint = useScrollStore((s) => s.setActiveSnapPoint);
1972
+ const view = useMemo(() => views.find((v) => v.id === viewId), [views, viewId]);
1973
+ const viewProgress = view?.progress ?? 0;
1974
+ view?.activeSnapPointId ?? null;
1975
+ const sortedPoints = useMemo(
1976
+ () => [...points].sort((a, b) => a.position - b.position),
1977
+ [points]
1978
+ );
1979
+ const activePointData = useMemo(() => {
1980
+ if (sortedPoints.length === 0) {
1981
+ return { point: null, index: -1 };
1982
+ }
1983
+ let closestIndex = 0;
1984
+ let closestDistance = Math.abs(viewProgress - sortedPoints[0].position);
1985
+ for (let i = 1; i < sortedPoints.length; i++) {
1986
+ const distance = Math.abs(viewProgress - sortedPoints[i].position);
1987
+ if (distance < closestDistance) {
1988
+ closestDistance = distance;
1989
+ closestIndex = i;
1990
+ }
1991
+ }
1992
+ if (closestDistance <= snapThreshold) {
1993
+ return { point: sortedPoints[closestIndex], index: closestIndex };
1994
+ }
1995
+ for (let i = sortedPoints.length - 1; i >= 0; i--) {
1996
+ if (viewProgress >= sortedPoints[i].position - snapThreshold) {
1997
+ return { point: sortedPoints[i], index: i };
1998
+ }
1999
+ }
2000
+ return { point: sortedPoints[0], index: 0 };
2001
+ }, [viewProgress, sortedPoints, snapThreshold]);
2002
+ useEffect(() => {
2003
+ const newPointId = activePointData.point?.id ?? null;
2004
+ if (newPointId !== lastActivePointRef.current) {
2005
+ if (lastActivePointRef.current) {
2006
+ const prevPoint = sortedPoints.find((p) => p.id === lastActivePointRef.current);
2007
+ prevPoint?.onLeave?.();
2008
+ }
2009
+ setActiveSnapPoint(viewId, newPointId);
2010
+ if (newPointId) {
2011
+ const newPoint = sortedPoints.find((p) => p.id === newPointId);
2012
+ newPoint?.onReach?.();
2013
+ }
2014
+ lastActivePointRef.current = newPointId;
2015
+ }
2016
+ }, [activePointData.point?.id, viewId, sortedPoints, setActiveSnapPoint]);
2017
+ const goToPoint = useCallback((pointId) => {
2018
+ const point = sortedPoints.find((p) => p.id === pointId);
2019
+ if (!point || !scrollContainerRef.current) return;
2020
+ const container = scrollContainerRef.current;
2021
+ const maxScroll = container.scrollHeight - container.clientHeight;
2022
+ const targetScroll = point.position * maxScroll;
2023
+ container.scrollTo({
2024
+ top: targetScroll,
2025
+ behavior: smoothScroll ? "smooth" : "auto"
2026
+ });
2027
+ }, [sortedPoints, smoothScroll]);
2028
+ const goToNextPoint = useCallback(() => {
2029
+ const nextIndex = Math.min(activePointData.index + 1, sortedPoints.length - 1);
2030
+ const nextPoint = sortedPoints[nextIndex];
2031
+ if (nextPoint) {
2032
+ goToPoint(nextPoint.id);
2033
+ }
2034
+ }, [activePointData.index, sortedPoints, goToPoint]);
2035
+ const goToPrevPoint = useCallback(() => {
2036
+ const prevIndex = Math.max(activePointData.index - 1, 0);
2037
+ const prevPoint = sortedPoints[prevIndex];
2038
+ if (prevPoint) {
2039
+ goToPoint(prevPoint.id);
2040
+ }
2041
+ }, [activePointData.index, sortedPoints, goToPoint]);
2042
+ const pointsWithState = useMemo(
2043
+ () => sortedPoints.map((point) => ({
2044
+ ...point,
2045
+ isActive: point.id === activePointData.point?.id
2046
+ })),
2047
+ [sortedPoints, activePointData.point?.id]
2048
+ );
2049
+ return {
2050
+ activePoint: activePointData.point,
2051
+ activeIndex: activePointData.index,
2052
+ goToPoint,
2053
+ goToNextPoint,
2054
+ goToPrevPoint,
2055
+ points: pointsWithState,
2056
+ progress: viewProgress
2057
+ };
2058
+ }
2059
+ function createSnapPoints(positions, options) {
2060
+ const { prefix = "snap", labels } = options ?? {};
2061
+ return positions.map((position, index) => ({
2062
+ id: `${prefix}-${index}`,
2063
+ position,
2064
+ label: labels?.[index] ?? `Section ${index + 1}`
2065
+ }));
2066
+ }
2067
+
2068
+ 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 };