scroll-system 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1520 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AriaLiveRegion: () => AriaLiveRegion,
24
+ ControlledView: () => ControlledView,
25
+ DEFAULT_PROGRESS_DEBOUNCE: () => DEFAULT_PROGRESS_DEBOUNCE,
26
+ DEFAULT_TRANSITION_DURATION: () => DEFAULT_TRANSITION_DURATION,
27
+ DEFAULT_TRANSITION_EASING: () => DEFAULT_TRANSITION_EASING,
28
+ FullView: () => FullView,
29
+ LazyView: () => LazyView,
30
+ NAVIGATION_COOLDOWN: () => NAVIGATION_COOLDOWN,
31
+ NAV_THRESHOLDS: () => NAV_THRESHOLDS,
32
+ ScrollContainer: () => ScrollContainer,
33
+ ScrollDebugOverlay: () => ScrollDebugOverlay,
34
+ ScrollLockedView: () => ScrollLockedView,
35
+ selectActiveView: () => selectActiveView,
36
+ selectActiveViewProgress: () => selectActiveViewProgress,
37
+ selectCanNavigateNext: () => selectCanNavigateNext,
38
+ selectCanNavigatePrevious: () => selectCanNavigatePrevious,
39
+ useActiveViewProgress: () => useActiveViewProgress,
40
+ useDragHandler: () => useDragHandler,
41
+ useFocusManagement: () => useFocusManagement,
42
+ useHashSync: () => useHashSync,
43
+ useKeyboardHandler: () => useKeyboardHandler,
44
+ useMetricsReporter: () => useMetricsReporter,
45
+ useNavigation: () => useNavigation,
46
+ useScrollAnalytics: () => useScrollAnalytics,
47
+ useScrollStore: () => useScrollStore,
48
+ useScrollSystem: () => useScrollSystem,
49
+ useTouchHandler: () => useTouchHandler,
50
+ useViewControl: () => useViewControl,
51
+ useViewProgress: () => useViewProgress,
52
+ useViewRegistration: () => useViewRegistration,
53
+ useWheelHandler: () => useWheelHandler
54
+ });
55
+ module.exports = __toCommonJS(index_exports);
56
+
57
+ // components/ScrollContainer.tsx
58
+ var import_react8 = require("react");
59
+
60
+ // store/navigation.store.ts
61
+ var import_zustand = require("zustand");
62
+ var import_middleware = require("zustand/middleware");
63
+
64
+ // constants.ts
65
+ var DEFAULT_TRANSITION_DURATION = 700;
66
+ var DEFAULT_TRANSITION_EASING = "cubic-bezier(0.645, 0.045, 0.355, 1.000)";
67
+ var DEFAULT_PROGRESS_DEBOUNCE = 16;
68
+ var NAVIGATION_COOLDOWN = 500;
69
+ var NAV_THRESHOLDS = {
70
+ WHEEL: 60,
71
+ // Acumulado de deltaY para disparar navegación
72
+ TOUCH: 50
73
+ // Píxeles de distancia para considerar swipe
74
+ };
75
+
76
+ // store/navigation.store.ts
77
+ function evaluateStateMachine(capability, progress, viewType, explicitLock) {
78
+ if (explicitLock) return explicitLock;
79
+ if (capability === "none") return "unlocked";
80
+ if (viewType === "full") return "unlocked";
81
+ if (progress >= 0.99) return "unlocked";
82
+ return "locked";
83
+ }
84
+ function calculateCapability(metrics) {
85
+ if (metrics.scrollHeight - metrics.clientHeight <= 1) {
86
+ return "none";
87
+ }
88
+ return "internal";
89
+ }
90
+ function calculateProgress(metrics) {
91
+ const maxScroll = metrics.scrollHeight - metrics.clientHeight;
92
+ if (maxScroll <= 1) return 1;
93
+ return Math.max(0, Math.min(1, metrics.scrollTop / maxScroll));
94
+ }
95
+ var initialState = {
96
+ views: [],
97
+ activeIndex: 0,
98
+ activeId: null,
99
+ totalViews: 0,
100
+ isInitialized: false,
101
+ isTransitioning: false,
102
+ isGlobalLocked: false,
103
+ isDragging: false,
104
+ globalProgress: 0
105
+ };
106
+ var lastNavigationTime = 0;
107
+ var useScrollStore = (0, import_zustand.create)()(
108
+ (0, import_middleware.subscribeWithSelector)((set, get) => ({
109
+ ...initialState,
110
+ initialize: () => {
111
+ const { views } = get();
112
+ if (views.length > 0) {
113
+ set({
114
+ isInitialized: true,
115
+ activeId: views[0]?.id ?? null,
116
+ activeIndex: 0
117
+ });
118
+ }
119
+ },
120
+ registerView: (config) => {
121
+ set((state) => {
122
+ if (state.views.some((v) => v.id === config.id)) return state;
123
+ const newIndex = state.views.length;
124
+ const newView = {
125
+ id: config.id,
126
+ index: newIndex,
127
+ type: config.type,
128
+ isActive: newIndex === 0,
129
+ capability: "none",
130
+ navigation: "unlocked",
131
+ explicitLock: null,
132
+ progress: 0,
133
+ metrics: { scrollHeight: 0, clientHeight: 0, scrollTop: 0 },
134
+ config
135
+ };
136
+ const newViews = [...state.views, newView];
137
+ return {
138
+ views: newViews,
139
+ totalViews: newViews.length,
140
+ activeId: state.activeId ?? newView.id
141
+ };
142
+ });
143
+ },
144
+ unregisterView: (id) => {
145
+ set((state) => {
146
+ const newViews = state.views.filter((v) => v.id !== id).map((v, idx) => ({ ...v, index: idx }));
147
+ const newActiveIndex = Math.min(state.activeIndex, newViews.length - 1);
148
+ return {
149
+ views: newViews,
150
+ totalViews: newViews.length,
151
+ activeIndex: Math.max(0, newActiveIndex),
152
+ activeId: newViews[newActiveIndex]?.id ?? null
153
+ };
154
+ });
155
+ },
156
+ updateViewMetrics: (id, metrics) => {
157
+ set((state) => {
158
+ const viewIndex = state.views.findIndex((v) => v.id === id);
159
+ if (viewIndex === -1) return state;
160
+ const view = state.views[viewIndex];
161
+ const capability = calculateCapability(metrics);
162
+ const progress = calculateProgress(metrics);
163
+ const navigation = evaluateStateMachine(capability, progress, view.type, view.explicitLock);
164
+ if (view.capability === capability && Math.abs(view.progress - progress) < 1e-4 && view.navigation === navigation) {
165
+ return state;
166
+ }
167
+ const newViews = [...state.views];
168
+ newViews[viewIndex] = {
169
+ ...view,
170
+ metrics,
171
+ capability,
172
+ progress,
173
+ navigation
174
+ };
175
+ return { views: newViews };
176
+ });
177
+ },
178
+ processIntention: (intention) => {
179
+ const state = get();
180
+ if (state.isTransitioning || state.isGlobalLocked) return false;
181
+ const activeView = state.views[state.activeIndex];
182
+ if (!activeView) return false;
183
+ if (intention.type === "navigate") {
184
+ if (intention.direction === "down") {
185
+ if (activeView.navigation === "locked") return false;
186
+ if (state.activeIndex < state.totalViews - 1) {
187
+ get().goToView(state.activeIndex + 1);
188
+ return true;
189
+ }
190
+ } else if (intention.direction === "up") {
191
+ const isAtTop = activeView.metrics.scrollTop <= 1;
192
+ if (activeView.capability === "internal" && !isAtTop) {
193
+ return false;
194
+ }
195
+ if (activeView.type === "controlled") {
196
+ const config = activeView.config;
197
+ if (config.allowGoBack === false) {
198
+ return false;
199
+ }
200
+ } else {
201
+ if (activeView.explicitLock === "locked") return false;
202
+ }
203
+ if (state.activeIndex > 0) {
204
+ get().goToView(state.activeIndex - 1);
205
+ return true;
206
+ }
207
+ }
208
+ }
209
+ return false;
210
+ },
211
+ goToNext: () => {
212
+ const state = get();
213
+ state.goToView(state.activeIndex + 1);
214
+ },
215
+ goToPrevious: () => {
216
+ const state = get();
217
+ state.goToView(state.activeIndex - 1);
218
+ },
219
+ goToView: (indexOrId) => {
220
+ const state = get();
221
+ const now = Date.now();
222
+ if (now - lastNavigationTime < NAVIGATION_COOLDOWN) return;
223
+ lastNavigationTime = now;
224
+ let targetIndex = typeof indexOrId === "string" ? state.views.findIndex((v) => v.id === indexOrId) : indexOrId;
225
+ if (targetIndex < 0 || targetIndex >= state.totalViews) return;
226
+ if (targetIndex === state.activeIndex) return;
227
+ set((s) => ({
228
+ ...s,
229
+ isTransitioning: true,
230
+ activeIndex: targetIndex,
231
+ activeId: s.views[targetIndex]?.id ?? null,
232
+ views: s.views.map((v) => {
233
+ if (v.index === targetIndex) return { ...v, isActive: true };
234
+ if (v.index === s.activeIndex) return { ...v, isActive: false };
235
+ return v;
236
+ })
237
+ }));
238
+ },
239
+ setViewExplicitLock: (id, lock) => {
240
+ set((state) => {
241
+ const index = state.views.findIndex((v) => v.id === id);
242
+ if (index === -1) return state;
243
+ const view = state.views[index];
244
+ const navigation = evaluateStateMachine(view.capability, view.progress, view.type, lock);
245
+ const newViews = [...state.views];
246
+ newViews[index] = { ...view, explicitLock: lock, navigation };
247
+ return { views: newViews };
248
+ });
249
+ },
250
+ setGlobalLock: (locked) => set({ isGlobalLocked: locked }),
251
+ setDragging: (dragging) => set({ isDragging: dragging }),
252
+ getViewById: (id) => get().views.find((v) => v.id === id),
253
+ startTransition: () => set({ isTransitioning: true }),
254
+ endTransition: () => set({ isTransitioning: false }),
255
+ resetNavigationCooldown: () => {
256
+ lastNavigationTime = 0;
257
+ }
258
+ }))
259
+ );
260
+ var selectActiveView = (state) => state.views[state.activeIndex];
261
+ var selectActiveViewProgress = (state) => state.views[state.activeIndex]?.progress ?? 0;
262
+ var selectCanNavigateNext = (state) => {
263
+ if (state.isTransitioning || state.isGlobalLocked) return false;
264
+ const activeView = state.views[state.activeIndex];
265
+ if (!activeView) return false;
266
+ return activeView.navigation === "unlocked";
267
+ };
268
+ var selectCanNavigatePrevious = (state) => {
269
+ if (state.isTransitioning || state.isGlobalLocked) return false;
270
+ return state.activeIndex > 0;
271
+ };
272
+
273
+ // hooks/useWheelHandler.ts
274
+ var import_react = require("react");
275
+
276
+ // utils/normalizeWheel.ts
277
+ function normalizeWheel(event) {
278
+ let pixelX = 0;
279
+ let pixelY = 0;
280
+ let pixelZ = 0;
281
+ if ("detail" in event) {
282
+ pixelY = event.detail;
283
+ }
284
+ if ("wheelDelta" in event) {
285
+ pixelY = -event.wheelDelta / 120;
286
+ }
287
+ if ("wheelDeltaY" in event) {
288
+ pixelY = -event.wheelDeltaY / 120;
289
+ }
290
+ if ("wheelDeltaX" in event) {
291
+ pixelX = -event.wheelDeltaX / 120;
292
+ }
293
+ if ("deltaY" in event) {
294
+ pixelY = event.deltaY;
295
+ }
296
+ if ("deltaX" in event) {
297
+ pixelX = event.deltaX;
298
+ }
299
+ if ((event.deltaMode || 0) === 1) {
300
+ pixelY *= 40;
301
+ pixelX *= 40;
302
+ } else if ((event.deltaMode || 0) === 2) {
303
+ pixelY *= 800;
304
+ pixelX *= 800;
305
+ }
306
+ return { pixelX, pixelY, pixelZ };
307
+ }
308
+
309
+ // hooks/useWheelHandler.ts
310
+ function useWheelHandler() {
311
+ const scrollAccumulator = (0, import_react.useRef)(0);
312
+ const lastScrollTime = (0, import_react.useRef)(0);
313
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
314
+ (0, import_react.useEffect)(() => {
315
+ const handleWheel = (event) => {
316
+ const state = useScrollStore.getState();
317
+ if (state.isTransitioning || state.isDragging) {
318
+ event.preventDefault();
319
+ return;
320
+ }
321
+ const normalized = normalizeWheel(event);
322
+ const delta = normalized.pixelY;
323
+ scrollAccumulator.current += delta;
324
+ const now = Date.now();
325
+ if (now - lastScrollTime.current > 200) {
326
+ scrollAccumulator.current = delta;
327
+ }
328
+ lastScrollTime.current = now;
329
+ if (Math.abs(scrollAccumulator.current) >= NAV_THRESHOLDS.WHEEL) {
330
+ const direction = scrollAccumulator.current > 0 ? "down" : "up";
331
+ const intention = {
332
+ type: "navigate",
333
+ direction,
334
+ strength: Math.min(Math.abs(scrollAccumulator.current) / NAV_THRESHOLDS.WHEEL, 1),
335
+ origin: "wheel"
336
+ };
337
+ const handled = useScrollStore.getState().processIntention(intention);
338
+ if (handled) {
339
+ scrollAccumulator.current = 0;
340
+ event.preventDefault();
341
+ } else {
342
+ }
343
+ }
344
+ };
345
+ window.addEventListener("wheel", handleWheel, { passive: false });
346
+ return () => window.removeEventListener("wheel", handleWheel);
347
+ }, []);
348
+ }
349
+
350
+ // hooks/useTouchHandler.ts
351
+ var import_react2 = require("react");
352
+ function useTouchHandler(options = {}) {
353
+ const { enabled = true } = options;
354
+ const touchStart = (0, import_react2.useRef)(null);
355
+ const touchStartTime = (0, import_react2.useRef)(0);
356
+ (0, import_react2.useEffect)(() => {
357
+ if (!enabled) return;
358
+ const handleTouchStart = (e) => {
359
+ touchStart.current = {
360
+ x: e.touches[0].clientX,
361
+ y: e.touches[0].clientY
362
+ };
363
+ touchStartTime.current = Date.now();
364
+ };
365
+ const handleTouchEnd = (e) => {
366
+ if (!touchStart.current) return;
367
+ const touchEnd = {
368
+ x: e.changedTouches[0].clientX,
369
+ y: e.changedTouches[0].clientY
370
+ };
371
+ const deltaY = touchStart.current.y - touchEnd.y;
372
+ const deltaX = touchStart.current.x - touchEnd.x;
373
+ const timeElapsed = Date.now() - touchStartTime.current;
374
+ if (Math.abs(deltaY) > Math.abs(deltaX) && // Vertical
375
+ Math.abs(deltaY) > NAV_THRESHOLDS.TOUCH && // Threshold distancia
376
+ timeElapsed < 800) {
377
+ const direction = deltaY > 0 ? "down" : "up";
378
+ const intention = {
379
+ type: "navigate",
380
+ direction,
381
+ strength: 1,
382
+ // Swipes son intenciones fuertes
383
+ origin: "touch"
384
+ };
385
+ useScrollStore.getState().processIntention(intention);
386
+ }
387
+ touchStart.current = null;
388
+ };
389
+ window.addEventListener("touchstart", handleTouchStart, { passive: true });
390
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
391
+ return () => {
392
+ window.removeEventListener("touchstart", handleTouchStart);
393
+ window.removeEventListener("touchend", handleTouchEnd);
394
+ };
395
+ }, [enabled]);
396
+ }
397
+
398
+ // hooks/useKeyboardHandler.ts
399
+ var import_react3 = require("react");
400
+ function useKeyboardHandler(options = {}) {
401
+ const { enabled = true, preventDefault = true } = options;
402
+ (0, import_react3.useEffect)(() => {
403
+ if (!enabled) return;
404
+ const handleKeyDown = (e) => {
405
+ const target = e.target;
406
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
407
+ return;
408
+ }
409
+ let intention = null;
410
+ switch (e.key) {
411
+ case "ArrowDown":
412
+ case "PageDown":
413
+ intention = {
414
+ type: "navigate",
415
+ direction: "down",
416
+ strength: 1,
417
+ origin: "keyboard"
418
+ };
419
+ break;
420
+ case "ArrowUp":
421
+ case "PageUp":
422
+ intention = {
423
+ type: "navigate",
424
+ direction: "up",
425
+ strength: 1,
426
+ origin: "keyboard"
427
+ };
428
+ break;
429
+ case " ":
430
+ intention = {
431
+ type: "navigate",
432
+ direction: e.shiftKey ? "up" : "down",
433
+ strength: 1,
434
+ origin: "keyboard"
435
+ };
436
+ break;
437
+ case "Home":
438
+ useScrollStore.getState().goToView(0);
439
+ if (preventDefault) e.preventDefault();
440
+ return;
441
+ case "End":
442
+ const totalViews = useScrollStore.getState().totalViews;
443
+ useScrollStore.getState().goToView(totalViews - 1);
444
+ if (preventDefault) e.preventDefault();
445
+ return;
446
+ default:
447
+ return;
448
+ }
449
+ if (intention) {
450
+ if (preventDefault) e.preventDefault();
451
+ useScrollStore.getState().processIntention(intention);
452
+ }
453
+ };
454
+ window.addEventListener("keydown", handleKeyDown);
455
+ return () => {
456
+ window.removeEventListener("keydown", handleKeyDown);
457
+ };
458
+ }, [enabled, preventDefault]);
459
+ }
460
+
461
+ // hooks/useHashSync.ts
462
+ var import_react4 = require("react");
463
+ function useHashSync(options = {}) {
464
+ const { enabled = true, pushHistory = false, hashPrefix = "" } = options;
465
+ const hasInitialized = (0, import_react4.useRef)(false);
466
+ (0, import_react4.useEffect)(() => {
467
+ if (!enabled) return;
468
+ const unsubscribe = useScrollStore.subscribe(
469
+ (state) => state.activeIndex,
470
+ (activeIndex, prevIndex) => {
471
+ if (!hasInitialized.current) return;
472
+ if (activeIndex === prevIndex) return;
473
+ const views = useScrollStore.getState().views;
474
+ const activeView = views[activeIndex];
475
+ if (activeView) {
476
+ const hash = `#${hashPrefix}${activeView.id}`;
477
+ if (pushHistory) {
478
+ window.history.pushState(null, "", hash);
479
+ } else {
480
+ window.history.replaceState(null, "", hash);
481
+ }
482
+ }
483
+ }
484
+ );
485
+ return () => unsubscribe();
486
+ }, [enabled, pushHistory, hashPrefix]);
487
+ (0, import_react4.useEffect)(() => {
488
+ if (!enabled) return;
489
+ const handlePopState = () => {
490
+ const hash = window.location.hash.slice(1);
491
+ if (!hash) return;
492
+ const viewId = hashPrefix ? hash.replace(hashPrefix, "") : hash;
493
+ const views = useScrollStore.getState().views;
494
+ const targetIndex = views.findIndex((v) => v.id === viewId);
495
+ if (targetIndex !== -1) {
496
+ useScrollStore.getState().goToView(targetIndex);
497
+ }
498
+ };
499
+ window.addEventListener("popstate", handlePopState);
500
+ return () => window.removeEventListener("popstate", handlePopState);
501
+ }, [enabled, hashPrefix]);
502
+ (0, import_react4.useEffect)(() => {
503
+ if (!enabled) return;
504
+ const checkAndNavigate = () => {
505
+ const state = useScrollStore.getState();
506
+ if (!state.isInitialized || state.views.length === 0) {
507
+ setTimeout(checkAndNavigate, 100);
508
+ return;
509
+ }
510
+ const hash = window.location.hash.slice(1);
511
+ if (!hash) {
512
+ hasInitialized.current = true;
513
+ return;
514
+ }
515
+ const viewId = hashPrefix ? hash.replace(hashPrefix, "") : hash;
516
+ const targetIndex = state.views.findIndex((v) => v.id === viewId);
517
+ if (targetIndex !== -1 && targetIndex !== state.activeIndex) {
518
+ state.goToView(targetIndex);
519
+ }
520
+ hasInitialized.current = true;
521
+ };
522
+ checkAndNavigate();
523
+ }, [enabled, hashPrefix]);
524
+ }
525
+
526
+ // hooks/useDragHandler.ts
527
+ var import_react5 = require("react");
528
+ var DRAG_THRESHOLD = 50;
529
+ var VELOCITY_THRESHOLD = 0.5;
530
+ var RESISTANCE_FACTOR = 0.4;
531
+ function useDragHandler(options = {}) {
532
+ const { enabled = true, onDragUpdate, onDragEnd } = options;
533
+ const [dragState, setDragState] = (0, import_react5.useState)({
534
+ isDragging: false,
535
+ dragOffset: 0,
536
+ dragDirection: null
537
+ });
538
+ const touchStartRef = (0, import_react5.useRef)(null);
539
+ const lastMoveRef = (0, import_react5.useRef)(null);
540
+ const rafRef = (0, import_react5.useRef)(null);
541
+ const updateDragState = (0, import_react5.useCallback)((newState) => {
542
+ setDragState((prev) => {
543
+ const updated = { ...prev, ...newState };
544
+ onDragUpdate?.(updated);
545
+ return updated;
546
+ });
547
+ }, [onDragUpdate]);
548
+ (0, import_react5.useEffect)(() => {
549
+ if (!enabled) return;
550
+ const handleTouchStart = (e) => {
551
+ const target = e.target;
552
+ if (target.closest('[data-scrollable="true"]')) return;
553
+ touchStartRef.current = {
554
+ y: e.touches[0].clientY,
555
+ time: Date.now()
556
+ };
557
+ lastMoveRef.current = touchStartRef.current;
558
+ useScrollStore.getState().setDragging(true);
559
+ updateDragState({ isDragging: true, dragOffset: 0, dragDirection: null });
560
+ };
561
+ const handleTouchMove = (e) => {
562
+ if (!touchStartRef.current) return;
563
+ const currentY = e.touches[0].clientY;
564
+ const deltaY = touchStartRef.current.y - currentY;
565
+ const viewportHeight = window.innerHeight;
566
+ let offset = deltaY / viewportHeight;
567
+ const store = useScrollStore.getState();
568
+ const atStart = store.activeIndex === 0 && deltaY < 0;
569
+ const atEnd = store.activeIndex === store.totalViews - 1 && deltaY > 0;
570
+ if (atStart || atEnd) {
571
+ offset = offset * RESISTANCE_FACTOR;
572
+ }
573
+ offset = Math.max(-1, Math.min(1, offset));
574
+ lastMoveRef.current = { y: currentY, time: Date.now() };
575
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
576
+ rafRef.current = requestAnimationFrame(() => {
577
+ updateDragState({
578
+ dragOffset: offset,
579
+ dragDirection: deltaY > 0 ? "down" : deltaY < 0 ? "up" : null
580
+ });
581
+ });
582
+ };
583
+ const handleTouchEnd = (e) => {
584
+ if (!touchStartRef.current || !lastMoveRef.current) {
585
+ updateDragState({ isDragging: false, dragOffset: 0, dragDirection: null });
586
+ return;
587
+ }
588
+ const endY = e.changedTouches[0].clientY;
589
+ const deltaY = touchStartRef.current.y - endY;
590
+ const timeDelta = Date.now() - lastMoveRef.current.time;
591
+ const velocity = timeDelta > 0 ? Math.abs(deltaY) / timeDelta : 0;
592
+ const store = useScrollStore.getState();
593
+ const atStart = store.activeIndex === 0;
594
+ const atEnd = store.activeIndex === store.totalViews - 1;
595
+ const exceedsThreshold = Math.abs(deltaY) > DRAG_THRESHOLD;
596
+ const hasVelocity = velocity > VELOCITY_THRESHOLD;
597
+ const direction = deltaY > 0 ? "down" : "up";
598
+ const canNavigate = direction === "down" && !atEnd || direction === "up" && !atStart;
599
+ const shouldNavigate = canNavigate && (exceedsThreshold || hasVelocity);
600
+ onDragEnd?.(shouldNavigate, direction);
601
+ if (shouldNavigate) {
602
+ store.processIntention({
603
+ type: "navigate",
604
+ direction,
605
+ strength: Math.min(1, velocity / VELOCITY_THRESHOLD),
606
+ origin: "touch"
607
+ });
608
+ }
609
+ touchStartRef.current = null;
610
+ lastMoveRef.current = null;
611
+ useScrollStore.getState().setDragging(false);
612
+ updateDragState({ isDragging: false, dragOffset: 0, dragDirection: null });
613
+ };
614
+ const handleTouchCancel = () => {
615
+ touchStartRef.current = null;
616
+ lastMoveRef.current = null;
617
+ useScrollStore.getState().setDragging(false);
618
+ updateDragState({ isDragging: false, dragOffset: 0, dragDirection: null });
619
+ };
620
+ window.addEventListener("touchstart", handleTouchStart, { passive: true });
621
+ window.addEventListener("touchmove", handleTouchMove, { passive: true });
622
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
623
+ window.addEventListener("touchcancel", handleTouchCancel, { passive: true });
624
+ return () => {
625
+ window.removeEventListener("touchstart", handleTouchStart);
626
+ window.removeEventListener("touchmove", handleTouchMove);
627
+ window.removeEventListener("touchend", handleTouchEnd);
628
+ window.removeEventListener("touchcancel", handleTouchCancel);
629
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
630
+ };
631
+ }, [enabled, updateDragState, onDragEnd]);
632
+ return dragState;
633
+ }
634
+
635
+ // hooks/useFocusManagement.ts
636
+ var import_react6 = require("react");
637
+ function useFocusManagement(options = {}) {
638
+ const { enabled = true, focusDelay = 100 } = options;
639
+ const activeIndex = useScrollStore((s) => s.activeIndex);
640
+ const activeId = useScrollStore((s) => s.activeId);
641
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
642
+ const prevIndexRef = (0, import_react6.useRef)(activeIndex);
643
+ (0, import_react6.useEffect)(() => {
644
+ if (!enabled) return;
645
+ if (activeIndex !== prevIndexRef.current && !isTransitioning && activeId) {
646
+ const timer = setTimeout(() => {
647
+ const viewElement = document.getElementById(activeId);
648
+ if (viewElement) {
649
+ if (!viewElement.hasAttribute("tabindex")) {
650
+ viewElement.setAttribute("tabindex", "-1");
651
+ }
652
+ viewElement.focus({ preventScroll: true });
653
+ }
654
+ prevIndexRef.current = activeIndex;
655
+ }, focusDelay);
656
+ return () => clearTimeout(timer);
657
+ }
658
+ }, [activeIndex, activeId, isTransitioning, enabled, focusDelay]);
659
+ }
660
+
661
+ // hooks/useScrollSystem.ts
662
+ var import_react7 = require("react");
663
+ function useScrollSystem() {
664
+ const activeIndex = useScrollStore((s) => s.activeIndex);
665
+ const globalProgress = useScrollStore((s) => s.globalProgress);
666
+ const isGlobalLocked = useScrollStore((s) => s.isGlobalLocked);
667
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
668
+ const isDragging = useScrollStore((s) => s.isDragging);
669
+ const activeId = useScrollStore((s) => s.activeId);
670
+ const totalViews = useScrollStore((s) => s.totalViews);
671
+ const views = useScrollStore((s) => s.views);
672
+ const {
673
+ goToNext: storeNext,
674
+ goToPrevious: storePrev,
675
+ goToView: storeGoTo
676
+ } = useScrollStore();
677
+ const activeView = views[activeIndex];
678
+ const activeViewType = activeView?.type ?? null;
679
+ const activeViewProgress = activeView?.progress ?? 0;
680
+ const canNavigateNext = useScrollStore(selectCanNavigateNext);
681
+ const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
682
+ const goToNext = (0, import_react7.useCallback)(() => {
683
+ if (canNavigateNext) {
684
+ storeNext();
685
+ return true;
686
+ }
687
+ return false;
688
+ }, [canNavigateNext, storeNext]);
689
+ const goToPrev = (0, import_react7.useCallback)(() => {
690
+ if (canNavigatePrevious) {
691
+ storePrev();
692
+ return true;
693
+ }
694
+ return false;
695
+ }, [canNavigatePrevious, storePrev]);
696
+ const getCurrentIndex = (0, import_react7.useCallback)(() => activeIndex, [activeIndex]);
697
+ const getProgress = (0, import_react7.useCallback)(() => globalProgress, [globalProgress]);
698
+ const getActiveViewProgress = (0, import_react7.useCallback)(() => activeViewProgress, [activeViewProgress]);
699
+ const isLocked = (0, import_react7.useCallback)(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
700
+ return (0, import_react7.useMemo)(() => ({
701
+ goToNext,
702
+ goToPrev,
703
+ goTo: storeGoTo,
704
+ getCurrentIndex,
705
+ getProgress,
706
+ getActiveViewProgress,
707
+ isLocked,
708
+ canGoNext: canNavigateNext,
709
+ canGoPrev: canNavigatePrevious,
710
+ // Extended Data for Nav/UI
711
+ activeIndex,
712
+ activeId,
713
+ activeViewType,
714
+ totalViews,
715
+ // State Flags (for advanced use)
716
+ isDragging,
717
+ isTransitioning
718
+ }), [
719
+ goToNext,
720
+ goToPrev,
721
+ storeGoTo,
722
+ getCurrentIndex,
723
+ getProgress,
724
+ getActiveViewProgress,
725
+ isLocked,
726
+ canNavigateNext,
727
+ canNavigatePrevious,
728
+ activeIndex,
729
+ activeId,
730
+ activeViewType,
731
+ totalViews,
732
+ isDragging,
733
+ isTransitioning
734
+ ]);
735
+ }
736
+
737
+ // utils/index.ts
738
+ function throttle(fn, limit) {
739
+ let lastCall = 0;
740
+ let timeoutId = null;
741
+ return ((...args) => {
742
+ const now = Date.now();
743
+ const remaining = limit - (now - lastCall);
744
+ if (remaining <= 0) {
745
+ if (timeoutId) {
746
+ clearTimeout(timeoutId);
747
+ timeoutId = null;
748
+ }
749
+ lastCall = now;
750
+ fn(...args);
751
+ } else if (!timeoutId) {
752
+ timeoutId = setTimeout(() => {
753
+ lastCall = Date.now();
754
+ timeoutId = null;
755
+ fn(...args);
756
+ }, remaining);
757
+ }
758
+ });
759
+ }
760
+ function prefersReducedMotion() {
761
+ if (typeof window === "undefined") return false;
762
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
763
+ }
764
+
765
+ // components/ScrollContainer.tsx
766
+ var import_jsx_runtime = require("react/jsx-runtime");
767
+ function ScrollContainer({
768
+ children,
769
+ className = "",
770
+ transitionDuration = DEFAULT_TRANSITION_DURATION,
771
+ transitionEasing = DEFAULT_TRANSITION_EASING,
772
+ onViewChange,
773
+ onInitialized,
774
+ // Deep Linking options
775
+ enableHashSync = false,
776
+ hashPushHistory = false,
777
+ hashPrefix = "",
778
+ // Accessibility
779
+ respectReducedMotion = true,
780
+ enableFocusManagement = true,
781
+ // Touch Physics
782
+ enableDragPhysics = false,
783
+ // Layout
784
+ orientation = "vertical"
785
+ }) {
786
+ const containerRef = (0, import_react8.useRef)(null);
787
+ const isBrowser = typeof window !== "undefined";
788
+ const [reducedMotion, setReducedMotion] = (0, import_react8.useState)(false);
789
+ (0, import_react8.useEffect)(() => {
790
+ if (!isBrowser || !respectReducedMotion) return;
791
+ setReducedMotion(prefersReducedMotion());
792
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
793
+ const handler = (e) => setReducedMotion(e.matches);
794
+ mediaQuery.addEventListener("change", handler);
795
+ return () => mediaQuery.removeEventListener("change", handler);
796
+ }, [respectReducedMotion, isBrowser]);
797
+ const effectiveDuration = reducedMotion ? 0 : transitionDuration;
798
+ const { initialize, endTransition } = useScrollStore();
799
+ const activeIndex = useScrollStore((s) => s.activeIndex);
800
+ const totalViews = useScrollStore((s) => s.totalViews);
801
+ const isInitialized = useScrollStore((s) => s.isInitialized);
802
+ const prevIndexRef = (0, import_react8.useRef)(activeIndex);
803
+ useWheelHandler();
804
+ useTouchHandler({ enabled: !enableDragPhysics });
805
+ useKeyboardHandler();
806
+ const dragState = useDragHandler({
807
+ enabled: enableDragPhysics && !reducedMotion
808
+ });
809
+ useHashSync({
810
+ enabled: enableHashSync,
811
+ pushHistory: hashPushHistory,
812
+ hashPrefix
813
+ });
814
+ useFocusManagement({ enabled: enableFocusManagement });
815
+ (0, import_react8.useEffect)(() => {
816
+ const timer = setTimeout(() => {
817
+ initialize();
818
+ onInitialized?.();
819
+ }, 50);
820
+ return () => clearTimeout(timer);
821
+ }, [initialize, onInitialized]);
822
+ (0, import_react8.useEffect)(() => {
823
+ if (prevIndexRef.current !== activeIndex) {
824
+ onViewChange?.(prevIndexRef.current, activeIndex);
825
+ const timer = setTimeout(() => {
826
+ endTransition();
827
+ }, effectiveDuration);
828
+ prevIndexRef.current = activeIndex;
829
+ return () => clearTimeout(timer);
830
+ }
831
+ }, [activeIndex, effectiveDuration, onViewChange, endTransition]);
832
+ const wrapperStyle = (0, import_react8.useMemo)(() => {
833
+ const baseOffset = activeIndex * 100;
834
+ const dragOffset = dragState.isDragging ? dragState.dragOffset * 100 : 0;
835
+ const transformAxis = orientation === "horizontal" ? "X" : "Y";
836
+ const sizeUnit = orientation === "horizontal" ? "vw" : "vh";
837
+ return {
838
+ transform: `translate${transformAxis}(-${baseOffset + dragOffset}${sizeUnit})`,
839
+ transition: dragState.isDragging ? "none" : effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none",
840
+ height: "100%",
841
+ width: "100%",
842
+ display: orientation === "horizontal" ? "flex" : "block",
843
+ flexDirection: orientation === "horizontal" ? "row" : void 0
844
+ };
845
+ }, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation]);
846
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
847
+ "div",
848
+ {
849
+ ref: containerRef,
850
+ className: `scroll-container fixed inset-0 overflow-hidden w-screen h-screen ${className}`,
851
+ role: "main",
852
+ "aria-label": "Scroll container",
853
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "scroll-wrapper", style: wrapperStyle, children })
854
+ }
855
+ );
856
+ }
857
+
858
+ // components/FullView.tsx
859
+ var import_react10 = require("react");
860
+
861
+ // hooks/useViewRegistration.ts
862
+ var import_react9 = require("react");
863
+ function useViewRegistration({
864
+ config,
865
+ onActivate,
866
+ onDeactivate,
867
+ onEnterStart,
868
+ onEnterEnd,
869
+ onExitStart,
870
+ onExitEnd
871
+ }) {
872
+ const registerView = useScrollStore((s) => s.registerView);
873
+ const unregisterView = useScrollStore((s) => s.unregisterView);
874
+ const activeId = useScrollStore((s) => s.activeId);
875
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
876
+ const callbacksRef = (0, import_react9.useRef)({
877
+ onActivate,
878
+ onDeactivate,
879
+ onEnterStart,
880
+ onEnterEnd,
881
+ onExitStart,
882
+ onExitEnd
883
+ });
884
+ callbacksRef.current = {
885
+ onActivate,
886
+ onDeactivate,
887
+ onEnterStart,
888
+ onEnterEnd,
889
+ onExitStart,
890
+ onExitEnd
891
+ };
892
+ const wasActiveRef = (0, import_react9.useRef)(false);
893
+ const wasTransitioningRef = (0, import_react9.useRef)(false);
894
+ const configRef = (0, import_react9.useRef)(config);
895
+ if (JSON.stringify(config) !== JSON.stringify(configRef.current)) {
896
+ configRef.current = config;
897
+ }
898
+ (0, import_react9.useEffect)(() => {
899
+ const currentConfig = configRef.current;
900
+ registerView(currentConfig);
901
+ return () => unregisterView(currentConfig.id);
902
+ }, [registerView, unregisterView, configRef.current]);
903
+ const viewState = useScrollStore((s) => s.views.find((v) => v.id === config.id));
904
+ const isActive = activeId === config.id;
905
+ (0, import_react9.useEffect)(() => {
906
+ if (isActive && !wasActiveRef.current) {
907
+ callbacksRef.current.onActivate?.();
908
+ } else if (!isActive && wasActiveRef.current) {
909
+ callbacksRef.current.onDeactivate?.();
910
+ }
911
+ wasActiveRef.current = isActive;
912
+ }, [isActive]);
913
+ (0, import_react9.useEffect)(() => {
914
+ const callbacks = callbacksRef.current;
915
+ const wasActive = wasActiveRef.current;
916
+ const wasTransitioning = wasTransitioningRef.current;
917
+ if (isTransitioning && !wasTransitioning) {
918
+ if (isActive && !wasActive) {
919
+ callbacks.onEnterStart?.();
920
+ } else if (!isActive && wasActive) {
921
+ callbacks.onExitStart?.();
922
+ }
923
+ }
924
+ if (!isTransitioning && wasTransitioning) {
925
+ if (isActive) {
926
+ callbacks.onEnterEnd?.();
927
+ } else if (wasActive && !isActive) {
928
+ callbacks.onExitEnd?.();
929
+ }
930
+ }
931
+ wasTransitioningRef.current = isTransitioning;
932
+ }, [isTransitioning, isActive]);
933
+ return {
934
+ isActive,
935
+ viewState,
936
+ index: viewState?.index ?? -1,
937
+ scrollProgress: viewState?.progress ?? 0,
938
+ navigation: viewState?.navigation ?? "unlocked"
939
+ };
940
+ }
941
+
942
+ // components/FullView.tsx
943
+ var import_jsx_runtime2 = require("react/jsx-runtime");
944
+ function FullView({
945
+ id,
946
+ children,
947
+ className = "",
948
+ meta,
949
+ onActivate,
950
+ onDeactivate,
951
+ onEnterStart,
952
+ onEnterEnd,
953
+ onExitStart,
954
+ onExitEnd
955
+ }) {
956
+ const config = (0, import_react10.useMemo)(
957
+ () => ({
958
+ id,
959
+ type: "full",
960
+ meta
961
+ }),
962
+ [id, meta]
963
+ );
964
+ const { isActive, index } = useViewRegistration({
965
+ config,
966
+ onActivate,
967
+ onDeactivate,
968
+ onEnterStart,
969
+ onEnterEnd,
970
+ onExitStart,
971
+ onExitEnd
972
+ });
973
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
974
+ "section",
975
+ {
976
+ id,
977
+ className: `
978
+ full-view
979
+ relative
980
+ w-full h-screen
981
+ overflow-hidden
982
+ ${isActive ? "is-active" : ""}
983
+ ${className}
984
+ `,
985
+ "data-view-type": "full",
986
+ "data-view-index": index,
987
+ "data-view-active": isActive,
988
+ "aria-hidden": !isActive,
989
+ children
990
+ }
991
+ );
992
+ }
993
+
994
+ // hooks/useMetricsReporter.ts
995
+ var import_react11 = require("react");
996
+ var SCROLL_THROTTLE_MS = 66;
997
+ function useMetricsReporter({
998
+ id,
999
+ isActive,
1000
+ scrollDirection,
1001
+ onScrollProgress,
1002
+ throttleMs = SCROLL_THROTTLE_MS
1003
+ }) {
1004
+ const scrollRef = (0, import_react11.useRef)(null);
1005
+ const updateMetrics = useScrollStore((s) => s.updateViewMetrics);
1006
+ const measureAndReport = (0, import_react11.useCallback)(() => {
1007
+ const el = scrollRef.current;
1008
+ if (!el) return;
1009
+ const metrics = {
1010
+ scrollHeight: scrollDirection === "vertical" ? el.scrollHeight : el.scrollWidth,
1011
+ clientHeight: scrollDirection === "vertical" ? el.clientHeight : el.clientWidth,
1012
+ scrollTop: scrollDirection === "vertical" ? el.scrollTop : el.scrollLeft
1013
+ };
1014
+ updateMetrics(id, metrics);
1015
+ if (onScrollProgress) {
1016
+ const max = metrics.scrollHeight - metrics.clientHeight;
1017
+ const progress = max > 0 ? metrics.scrollTop / max : 1;
1018
+ onScrollProgress(progress);
1019
+ }
1020
+ }, [id, scrollDirection, updateMetrics, onScrollProgress]);
1021
+ const throttledMeasure = (0, import_react11.useMemo)(
1022
+ () => throttle(measureAndReport, throttleMs),
1023
+ [measureAndReport, throttleMs]
1024
+ );
1025
+ (0, import_react11.useEffect)(() => {
1026
+ const el = scrollRef.current;
1027
+ if (!el) return;
1028
+ const resizeObserver = new ResizeObserver(() => {
1029
+ window.requestAnimationFrame(measureAndReport);
1030
+ });
1031
+ resizeObserver.observe(el);
1032
+ Array.from(el.children).forEach((child) => resizeObserver.observe(child));
1033
+ el.addEventListener("scroll", throttledMeasure, { passive: true });
1034
+ measureAndReport();
1035
+ return () => {
1036
+ resizeObserver.disconnect();
1037
+ el.removeEventListener("scroll", throttledMeasure);
1038
+ };
1039
+ }, [measureAndReport, throttledMeasure]);
1040
+ (0, import_react11.useEffect)(() => {
1041
+ if (isActive) {
1042
+ measureAndReport();
1043
+ }
1044
+ }, [isActive, measureAndReport]);
1045
+ return { scrollRef, measureAndReport };
1046
+ }
1047
+
1048
+ // components/ScrollLockedView.tsx
1049
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1050
+ function ScrollLockedView({
1051
+ id,
1052
+ children,
1053
+ className = "",
1054
+ scrollDirection = "vertical",
1055
+ scrollEndThreshold = 0.99,
1056
+ onScrollProgress,
1057
+ onActivate,
1058
+ onDeactivate,
1059
+ onEnterStart,
1060
+ onEnterEnd,
1061
+ onExitStart,
1062
+ onExitEnd
1063
+ }) {
1064
+ const { isActive, index } = useViewRegistration({
1065
+ config: {
1066
+ id,
1067
+ type: "scroll-locked",
1068
+ scrollDirection,
1069
+ scrollEndThreshold
1070
+ },
1071
+ onActivate,
1072
+ onDeactivate,
1073
+ onEnterStart,
1074
+ onEnterEnd,
1075
+ onExitStart,
1076
+ onExitEnd
1077
+ });
1078
+ const { scrollRef } = useMetricsReporter({
1079
+ id,
1080
+ isActive,
1081
+ scrollDirection,
1082
+ onScrollProgress
1083
+ });
1084
+ const scrollClasses = scrollDirection === "vertical" ? "overflow-y-auto overflow-x-hidden" : "overflow-x-auto overflow-y-hidden";
1085
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1086
+ "section",
1087
+ {
1088
+ id,
1089
+ className: `relative w-full h-screen ${className}`,
1090
+ "data-view-type": "scroll-locked",
1091
+ "data-active": isActive,
1092
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1093
+ "div",
1094
+ {
1095
+ ref: scrollRef,
1096
+ className: `w-full h-full ${scrollClasses} no-scrollbar`,
1097
+ style: { scrollbarWidth: "none" },
1098
+ children
1099
+ }
1100
+ )
1101
+ }
1102
+ );
1103
+ }
1104
+
1105
+ // components/ControlledView.tsx
1106
+ var import_react12 = require("react");
1107
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1108
+ function ControlledView({
1109
+ id,
1110
+ children,
1111
+ className = "",
1112
+ scrollDirection = "none",
1113
+ allowInternalScroll = false,
1114
+ canProceed = false,
1115
+ allowGoBack = true,
1116
+ onActivate,
1117
+ onDeactivate,
1118
+ onEnterStart,
1119
+ onEnterEnd,
1120
+ onExitStart,
1121
+ onExitEnd
1122
+ }) {
1123
+ const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1124
+ const config = (0, import_react12.useMemo)(
1125
+ () => ({
1126
+ id,
1127
+ type: "controlled",
1128
+ scrollDirection,
1129
+ allowInternalScroll,
1130
+ allowGoBack
1131
+ }),
1132
+ [id, scrollDirection, allowInternalScroll, allowGoBack]
1133
+ );
1134
+ const { isActive, index } = useViewRegistration({
1135
+ config,
1136
+ onActivate,
1137
+ onDeactivate,
1138
+ onEnterStart,
1139
+ onEnterEnd,
1140
+ onExitStart,
1141
+ onExitEnd
1142
+ });
1143
+ const { scrollRef } = useMetricsReporter({
1144
+ id,
1145
+ isActive,
1146
+ scrollDirection: allowInternalScroll ? scrollDirection : "none"
1147
+ });
1148
+ (0, import_react12.useEffect)(() => {
1149
+ const lockState = canProceed ? "unlocked" : "locked";
1150
+ setExplicitLock(id, lockState);
1151
+ }, [id, canProceed, setExplicitLock]);
1152
+ const overflowClasses = (0, import_react12.useMemo)(() => {
1153
+ if (!allowInternalScroll) return "overflow-hidden";
1154
+ switch (scrollDirection) {
1155
+ case "vertical":
1156
+ return "overflow-y-auto overflow-x-hidden";
1157
+ case "horizontal":
1158
+ return "overflow-x-auto overflow-y-hidden";
1159
+ default:
1160
+ return "overflow-auto";
1161
+ }
1162
+ }, [allowInternalScroll, scrollDirection]);
1163
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1164
+ "section",
1165
+ {
1166
+ id,
1167
+ className: `relative w-full h-screen ${isActive ? "z-10" : "z-0"} ${className}`,
1168
+ "data-view-type": "controlled",
1169
+ "data-active": isActive,
1170
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1171
+ "div",
1172
+ {
1173
+ ref: scrollRef,
1174
+ className: `w-full h-full ${overflowClasses}`,
1175
+ children
1176
+ }
1177
+ )
1178
+ }
1179
+ );
1180
+ }
1181
+ function useViewControl(viewId) {
1182
+ const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
1183
+ const goToNext = useScrollStore((s) => s.goToNext);
1184
+ const goToPrevious = useScrollStore((s) => s.goToPrevious);
1185
+ const goToView = useScrollStore((s) => s.goToView);
1186
+ return (0, import_react12.useMemo)(
1187
+ () => ({
1188
+ unlock: () => setExplicitLock(viewId, "unlocked"),
1189
+ lock: () => setExplicitLock(viewId, "locked"),
1190
+ // Navegación Programática (Forzada)
1191
+ goNext: () => goToNext(),
1192
+ goPrev: () => goToPrevious(),
1193
+ goTo: (to) => goToView(to)
1194
+ }),
1195
+ [viewId, setExplicitLock, goToNext, goToPrevious, goToView]
1196
+ );
1197
+ }
1198
+
1199
+ // components/ScrollDebugOverlay.tsx
1200
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1201
+ function ScrollDebugOverlay({
1202
+ position = "bottom-left",
1203
+ visible = true
1204
+ }) {
1205
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1206
+ const totalViews = useScrollStore((s) => s.totalViews);
1207
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
1208
+ const isGlobalLocked = useScrollStore((s) => s.isGlobalLocked);
1209
+ const isInitialized = useScrollStore((s) => s.isInitialized);
1210
+ const views = useScrollStore((s) => s.views);
1211
+ const activeView = views[activeIndex];
1212
+ if (!visible) return null;
1213
+ const positionClasses = {
1214
+ "top-left": "top-4 left-4",
1215
+ "top-right": "top-4 right-4",
1216
+ "bottom-left": "bottom-4 left-4",
1217
+ "bottom-right": "bottom-4 right-4"
1218
+ };
1219
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1220
+ "div",
1221
+ {
1222
+ className: `fixed ${positionClasses[position]} z-9999 font-mono text-xs bg-black/90 text-green-400 p-3 rounded-lg border border-green-500/30 backdrop-blur-sm max-w-xs`,
1223
+ style: { pointerEvents: "none" },
1224
+ children: [
1225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-green-300 font-bold mb-2 flex items-center gap-2", children: [
1226
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
1227
+ "ScrollSystem Debug"
1228
+ ] }),
1229
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1 mb-3", children: [
1230
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "initialized", value: isInitialized }),
1231
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
1232
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "transitioning", value: isTransitioning }),
1233
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "globalLocked", value: isGlobalLocked })
1234
+ ] }),
1235
+ activeView && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
1237
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "id", value: activeView.id }),
1238
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "type", value: activeView.type }),
1239
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "capability", value: activeView.capability }),
1240
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "navigation", value: activeView.navigation }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
1242
+ activeView.explicitLock && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
1243
+ ] }),
1244
+ activeView?.metrics && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
1245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
1246
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
1248
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
1249
+ ] })
1250
+ ]
1251
+ }
1252
+ );
1253
+ }
1254
+ function Row({
1255
+ label,
1256
+ value,
1257
+ highlight = false
1258
+ }) {
1259
+ const displayValue = typeof value === "boolean" ? value ? "\u2713" : "\u2717" : String(value);
1260
+ const valueColor = typeof value === "boolean" ? value ? "text-green-400" : "text-red-400" : highlight ? "text-yellow-400" : "text-white";
1261
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-between gap-4", children: [
1262
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-green-500/70", children: [
1263
+ label,
1264
+ ":"
1265
+ ] }),
1266
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: valueColor, children: displayValue })
1267
+ ] });
1268
+ }
1269
+
1270
+ // components/AriaLiveRegion.tsx
1271
+ var import_react13 = require("react");
1272
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1273
+ function AriaLiveRegion({
1274
+ template = "Navigated to section {viewIndex} of {totalViews}",
1275
+ politeness = "polite"
1276
+ }) {
1277
+ const [announcement, setAnnouncement] = (0, import_react13.useState)("");
1278
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1279
+ const activeId = useScrollStore((s) => s.activeId);
1280
+ const totalViews = useScrollStore((s) => s.totalViews);
1281
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
1282
+ (0, import_react13.useEffect)(() => {
1283
+ if (!isTransitioning && activeId) {
1284
+ const message = template.replace("{viewIndex}", String(activeIndex + 1)).replace("{viewId}", activeId).replace("{totalViews}", String(totalViews));
1285
+ const timer = setTimeout(() => {
1286
+ setAnnouncement(message);
1287
+ }, 100);
1288
+ return () => clearTimeout(timer);
1289
+ }
1290
+ }, [activeIndex, activeId, totalViews, isTransitioning, template]);
1291
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1292
+ "div",
1293
+ {
1294
+ role: "status",
1295
+ "aria-live": politeness,
1296
+ "aria-atomic": "true",
1297
+ className: "sr-only",
1298
+ style: {
1299
+ position: "absolute",
1300
+ width: "1px",
1301
+ height: "1px",
1302
+ padding: 0,
1303
+ margin: "-1px",
1304
+ overflow: "hidden",
1305
+ clip: "rect(0, 0, 0, 0)",
1306
+ whiteSpace: "nowrap",
1307
+ border: 0
1308
+ },
1309
+ children: announcement
1310
+ }
1311
+ );
1312
+ }
1313
+
1314
+ // components/LazyView.tsx
1315
+ var import_react14 = require("react");
1316
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1317
+ function LazyView({
1318
+ viewId,
1319
+ buffer = 1,
1320
+ children,
1321
+ placeholder = null
1322
+ }) {
1323
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1324
+ const views = useScrollStore((s) => s.views);
1325
+ const shouldRender = (0, import_react14.useMemo)(() => {
1326
+ const viewIndex = views.findIndex((v) => v.id === viewId);
1327
+ if (viewIndex === -1) return false;
1328
+ const minIndex = Math.max(0, activeIndex - buffer);
1329
+ const maxIndex = Math.min(views.length - 1, activeIndex + buffer);
1330
+ return viewIndex >= minIndex && viewIndex <= maxIndex;
1331
+ }, [viewId, views, activeIndex, buffer]);
1332
+ if (!shouldRender) {
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: placeholder });
1334
+ }
1335
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
1336
+ }
1337
+
1338
+ // hooks/useNavigation.ts
1339
+ var import_react15 = require("react");
1340
+ function useNavigation() {
1341
+ const goToView = useScrollStore((s) => s.goToView);
1342
+ const goToNextAction = useScrollStore((s) => s.goToNext);
1343
+ const goToPreviousAction = useScrollStore((s) => s.goToPrevious);
1344
+ const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
1345
+ const lockScroll = (0, import_react15.useCallback)(() => setGlobalLock(true), [setGlobalLock]);
1346
+ const unlockScroll = (0, import_react15.useCallback)(() => setGlobalLock(false), [setGlobalLock]);
1347
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1348
+ const activeId = useScrollStore((s) => s.activeId);
1349
+ const totalViews = useScrollStore((s) => s.totalViews);
1350
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
1351
+ const isScrollLocked = useScrollStore((s) => s.isGlobalLocked);
1352
+ const canNavigateNext = useScrollStore(selectCanNavigateNext);
1353
+ const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
1354
+ const goToNext = (0, import_react15.useCallback)(() => {
1355
+ return goToNextAction();
1356
+ }, [goToNextAction]);
1357
+ const goToPrevious = (0, import_react15.useCallback)(() => {
1358
+ return goToPreviousAction();
1359
+ }, [goToPreviousAction]);
1360
+ const isFirstView = activeIndex === 0;
1361
+ const isLastView = activeIndex === totalViews - 1;
1362
+ const progress = totalViews > 1 ? activeIndex / (totalViews - 1) : 0;
1363
+ return (0, import_react15.useMemo)(
1364
+ () => ({
1365
+ // Acciones de navegación
1366
+ goToView,
1367
+ goToNext,
1368
+ goToPrevious,
1369
+ // Control de bloqueo
1370
+ lockScroll,
1371
+ unlockScroll,
1372
+ // Estado actual
1373
+ activeIndex,
1374
+ activeId,
1375
+ totalViews,
1376
+ // Estados de UI
1377
+ isTransitioning,
1378
+ isScrollLocked,
1379
+ canNavigateNext,
1380
+ canNavigatePrevious,
1381
+ // Utilidades
1382
+ isFirstView,
1383
+ isLastView,
1384
+ progress
1385
+ }),
1386
+ [
1387
+ goToView,
1388
+ goToNext,
1389
+ goToPrevious,
1390
+ lockScroll,
1391
+ unlockScroll,
1392
+ activeIndex,
1393
+ activeId,
1394
+ totalViews,
1395
+ isTransitioning,
1396
+ isScrollLocked,
1397
+ canNavigateNext,
1398
+ canNavigatePrevious,
1399
+ isFirstView,
1400
+ isLastView,
1401
+ progress
1402
+ ]
1403
+ );
1404
+ }
1405
+
1406
+ // hooks/useViewProgress.ts
1407
+ function useViewProgress(viewId) {
1408
+ const progress = useScrollStore(
1409
+ (s) => s.views.find((v) => v.id === viewId)?.progress ?? 0
1410
+ );
1411
+ const navigation = useScrollStore(
1412
+ (s) => s.views.find((v) => v.id === viewId)?.navigation ?? "unlocked"
1413
+ );
1414
+ const isAtStart = progress <= 0.02;
1415
+ const isAtEnd = progress >= 0.99;
1416
+ return {
1417
+ progress,
1418
+ isAtStart,
1419
+ isAtEnd,
1420
+ navigation
1421
+ };
1422
+ }
1423
+ function useActiveViewProgress() {
1424
+ const progress = useScrollStore(selectActiveViewProgress);
1425
+ const activeView = useScrollStore((s) => s.views[s.activeIndex]);
1426
+ const hasInternalScroll = activeView?.capability === "internal";
1427
+ return {
1428
+ progress,
1429
+ hasInternalScroll,
1430
+ viewType: activeView?.type
1431
+ };
1432
+ }
1433
+
1434
+ // hooks/useScrollAnalytics.ts
1435
+ var import_react16 = require("react");
1436
+ function useScrollAnalytics(options = {}) {
1437
+ const { onViewEnter, onViewExit, enabled = true } = options;
1438
+ const activeIndex = useScrollStore((s) => s.activeIndex);
1439
+ const activeId = useScrollStore((s) => s.activeId);
1440
+ const isTransitioning = useScrollStore((s) => s.isTransitioning);
1441
+ const enterTimeRef = (0, import_react16.useRef)(Date.now());
1442
+ const prevIndexRef = (0, import_react16.useRef)(activeIndex);
1443
+ const prevIdRef = (0, import_react16.useRef)(activeId);
1444
+ const createAnalytics = (0, import_react16.useCallback)((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
1445
+ viewId,
1446
+ viewIndex,
1447
+ enterTime,
1448
+ exitTime,
1449
+ duration: exitTime ? (exitTime - enterTime) / 1e3 : 0,
1450
+ isActive
1451
+ }), []);
1452
+ (0, import_react16.useEffect)(() => {
1453
+ if (!enabled) return;
1454
+ if (!isTransitioning && (activeIndex !== prevIndexRef.current || activeId !== prevIdRef.current)) {
1455
+ const now = Date.now();
1456
+ if (prevIdRef.current) {
1457
+ const exitAnalytics = createAnalytics(
1458
+ prevIdRef.current,
1459
+ prevIndexRef.current,
1460
+ enterTimeRef.current,
1461
+ false,
1462
+ now
1463
+ );
1464
+ onViewExit?.(exitAnalytics);
1465
+ }
1466
+ if (activeId) {
1467
+ enterTimeRef.current = now;
1468
+ const enterAnalytics = createAnalytics(
1469
+ activeId,
1470
+ activeIndex,
1471
+ now,
1472
+ true
1473
+ );
1474
+ onViewEnter?.(enterAnalytics);
1475
+ }
1476
+ prevIndexRef.current = activeIndex;
1477
+ prevIdRef.current = activeId;
1478
+ }
1479
+ }, [activeIndex, activeId, isTransitioning, enabled, onViewEnter, onViewExit, createAnalytics]);
1480
+ return {
1481
+ currentViewId: activeId,
1482
+ currentViewIndex: activeIndex,
1483
+ viewStartTime: enterTimeRef.current,
1484
+ getTimeInView: () => (Date.now() - enterTimeRef.current) / 1e3
1485
+ };
1486
+ }
1487
+ // Annotate the CommonJS export names for ESM import in node:
1488
+ 0 && (module.exports = {
1489
+ AriaLiveRegion,
1490
+ ControlledView,
1491
+ DEFAULT_PROGRESS_DEBOUNCE,
1492
+ DEFAULT_TRANSITION_DURATION,
1493
+ DEFAULT_TRANSITION_EASING,
1494
+ FullView,
1495
+ LazyView,
1496
+ NAVIGATION_COOLDOWN,
1497
+ NAV_THRESHOLDS,
1498
+ ScrollContainer,
1499
+ ScrollDebugOverlay,
1500
+ ScrollLockedView,
1501
+ selectActiveView,
1502
+ selectActiveViewProgress,
1503
+ selectCanNavigateNext,
1504
+ selectCanNavigatePrevious,
1505
+ useActiveViewProgress,
1506
+ useDragHandler,
1507
+ useFocusManagement,
1508
+ useHashSync,
1509
+ useKeyboardHandler,
1510
+ useMetricsReporter,
1511
+ useNavigation,
1512
+ useScrollAnalytics,
1513
+ useScrollStore,
1514
+ useScrollSystem,
1515
+ useTouchHandler,
1516
+ useViewControl,
1517
+ useViewProgress,
1518
+ useViewRegistration,
1519
+ useWheelHandler
1520
+ });