react-sway 0.2.7 → 0.3.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.cjs CHANGED
@@ -1,505 +1,665 @@
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
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- ReactSway: () => ReactSway_default
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
-
27
- // src/ReactSway.tsx
28
- var import_react = require("react");
29
- var import_jsx_runtime = require("react/jsx-runtime");
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ let react_jsx_runtime = require("react/jsx-runtime");
4
+ //#region src/ReactSway.tsx
5
+ /**
6
+ * Core ReactSway component implementing axis-aware infinite scrolling interactions.
7
+ */
8
+ /** Velocity applied per arrow key press (pixels). */
30
9
  var ARROW_KEY_VELOCITY = 15;
31
- var DEFAULT_FRICTION = 0.95;
10
+ /** Default edge-hover activation thickness in pixels. */
11
+ var DEFAULT_EDGE_HOVER_SIZE = 96;
12
+ /** Default friction coefficient applied to velocity each frame. */
13
+ var DEFAULT_FRICTION = .95;
14
+ /** Default IntersectionObserver rootMargin for lazy visibility detection. */
32
15
  var DEFAULT_LAZY_ROOT_MARGIN = "100px";
33
- var DEFAULT_LAZY_THRESHOLD = 0.01;
16
+ /** Default IntersectionObserver threshold for lazy visibility detection. */
17
+ var DEFAULT_LAZY_THRESHOLD = .01;
18
+ /** Default delay in milliseconds before auto-scroll resumes after user interaction. */
34
19
  var DEFAULT_RESUME_DELAY = 2e3;
35
- var DEFAULT_SPEED = 0.5;
36
- var LOOP_SEGMENTS = 3;
20
+ /** Default scroll speed in pixels per frame at 60fps. */
21
+ var DEFAULT_SPEED = .5;
22
+ /** Number of stacked content groups used to build the seamless loop. */
23
+ var MIN_LOOP_SEGMENTS = 3;
24
+ /** Maximum deltaTime cap to prevent physics instability during frame drops. */
37
25
  var MAX_DELTA_TIME = 3;
26
+ /** Maximum allowed velocity magnitude to prevent runaway scrolling. */
38
27
  var MAX_VELOCITY = 150;
28
+ /** Duration of a single frame at 60fps in milliseconds. */
39
29
  var MS_PER_FRAME_60FPS = 16.667;
40
- var REDUCED_MOTION_SPEED_FACTOR = 0.25;
30
+ /** Speed multiplier when user prefers reduced motion (25% of normal). */
31
+ var REDUCED_MOTION_SPEED_FACTOR = .25;
32
+ /** Debounce delay in milliseconds for ResizeObserver callbacks. */
41
33
  var RESIZE_DEBOUNCE_MS = 150;
34
+ /** Fallback pixel height for wheel events reported in line units. */
42
35
  var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
36
+ /** WheelEvent deltaMode value for line-based deltas. */
43
37
  var WHEEL_MODE_LINE = 1;
38
+ /** WheelEvent deltaMode value for page-based deltas. */
44
39
  var WHEEL_MODE_PAGE = 2;
40
+ /** Minimum pixel impulse for discrete wheel hardware reporting tiny pixel deltas. */
45
41
  var WHEEL_PIXEL_MIN_DELTA_PX = 48;
46
- var WHEEL_VELOCITY_MULTIPLIER = 0.14;
47
- var WHEEL_IMMEDIATE_DELTA_FACTOR = 0.14;
42
+ /** Multiplier applied to wheel deltaY to convert to scroll velocity. */
43
+ var WHEEL_VELOCITY_MULTIPLIER = .14;
44
+ /** Small immediate wheel movement used to keep input responsive without jumping. */
45
+ var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
46
+ /** Legacy wheelDelta magnitude that usually represents one physical wheel notch. */
48
47
  var WHEEL_LEGACY_NOTCH_DELTA = 120;
48
+ /** Minimum touch movement before a gesture is classified by axis. */
49
+ var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
49
50
  function getLegacyWheelPixelDeltaY(event) {
50
- const { wheelDelta, wheelDeltaY } = event;
51
- const legacyWheelDelta = typeof wheelDeltaY === "number" ? wheelDeltaY : wheelDelta;
52
- if (typeof legacyWheelDelta !== "number" || !Number.isFinite(legacyWheelDelta)) {
53
- return 0;
54
- }
55
- return -(legacyWheelDelta / WHEEL_LEGACY_NOTCH_DELTA) * WHEEL_PIXEL_MIN_DELTA_PX;
51
+ const { wheelDelta, wheelDeltaY } = event;
52
+ const legacyWheelDelta = typeof wheelDeltaY === "number" ? wheelDeltaY : wheelDelta;
53
+ if (typeof legacyWheelDelta !== "number" || !Number.isFinite(legacyWheelDelta)) return 0;
54
+ return -(legacyWheelDelta / WHEEL_LEGACY_NOTCH_DELTA) * WHEEL_PIXEL_MIN_DELTA_PX;
56
55
  }
57
- function normalizeWheelDeltaY(event, container) {
58
- if (event.deltaMode === WHEEL_MODE_LINE) {
59
- const computedLineHeight = Number.parseFloat(window.getComputedStyle(container).lineHeight);
60
- const lineHeight = Number.isFinite(computedLineHeight) ? computedLineHeight : WHEEL_LINE_HEIGHT_FALLBACK_PX;
61
- return event.deltaY * lineHeight;
62
- }
63
- if (event.deltaMode === WHEEL_MODE_PAGE) {
64
- const pageHeight = Math.max(container.clientHeight, window.innerHeight, 1);
65
- return event.deltaY * pageHeight;
66
- }
67
- const legacyWheelPixelDeltaY = getLegacyWheelPixelDeltaY(event);
68
- if (Math.abs(event.deltaY) < WHEEL_PIXEL_MIN_DELTA_PX && Math.abs(legacyWheelPixelDeltaY) >= WHEEL_PIXEL_MIN_DELTA_PX) {
69
- return legacyWheelPixelDeltaY;
70
- }
71
- return event.deltaY;
56
+ /**
57
+ * Converts wheel deltas to pixels so mouse wheels, touchpads, and page-wheel
58
+ * devices feed the same Sway velocity system.
59
+ */
60
+ function normalizeWheelDelta(event, container, axis) {
61
+ const rawDelta = axis === "x" ? event.deltaX : event.deltaY;
62
+ if (event.deltaMode === WHEEL_MODE_LINE) {
63
+ const computedLineHeight = Number.parseFloat(window.getComputedStyle(container).lineHeight);
64
+ return rawDelta * (Number.isFinite(computedLineHeight) ? computedLineHeight : WHEEL_LINE_HEIGHT_FALLBACK_PX);
65
+ }
66
+ if (event.deltaMode === WHEEL_MODE_PAGE) return rawDelta * (axis === "x" ? Math.max(container.clientWidth, window.innerWidth, 1) : Math.max(container.clientHeight, window.innerHeight, 1));
67
+ if (axis === "x") return rawDelta;
68
+ const legacyWheelPixelDeltaY = getLegacyWheelPixelDeltaY(event);
69
+ if (Math.abs(rawDelta) < WHEEL_PIXEL_MIN_DELTA_PX && Math.abs(legacyWheelPixelDeltaY) >= WHEEL_PIXEL_MIN_DELTA_PX) return legacyWheelPixelDeltaY;
70
+ return rawDelta;
72
71
  }
73
- function ReactSway({
74
- autoScroll = true,
75
- children,
76
- direction = "up",
77
- draggable = true,
78
- friction = DEFAULT_FRICTION,
79
- keyboard = true,
80
- lazy = true,
81
- lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN,
82
- lazyThreshold = DEFAULT_LAZY_THRESHOLD,
83
- onPause,
84
- onResume,
85
- onScroll,
86
- pauseOnInteraction = true,
87
- resumeDelay = DEFAULT_RESUME_DELAY,
88
- speed = DEFAULT_SPEED,
89
- wheelEnabled = true
90
- }) {
91
- const normalizedFriction = (0, import_react.useMemo)(
92
- () => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION,
93
- [friction]
94
- );
95
- const normalizedResumeDelay = (0, import_react.useMemo)(
96
- () => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY,
97
- [resumeDelay]
98
- );
99
- const normalizedSpeed = (0, import_react.useMemo)(
100
- () => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED,
101
- [speed]
102
- );
103
- const [isDragging, setIsDragging] = (0, import_react.useState)(false);
104
- const [isPaused, setIsPaused] = (0, import_react.useState)(false);
105
- const [isTabActive, setIsTabActive] = (0, import_react.useState)(true);
106
- const [loopPoint, setLoopPoint] = (0, import_react.useState)(0);
107
- const [prefersReducedMotion, setPrefersReducedMotion] = (0, import_react.useState)(() => {
108
- if (typeof window === "undefined") return false;
109
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
110
- });
111
- const animationFrameRef = (0, import_react.useRef)(null);
112
- const autoScrollRef = (0, import_react.useRef)({ active: autoScroll, desired: autoScroll });
113
- const containerRef = (0, import_react.useRef)(null);
114
- const inactivityTimerRef = (0, import_react.useRef)(null);
115
- const isDraggingRef = (0, import_react.useRef)(false);
116
- const isPausedRef = (0, import_react.useRef)(false);
117
- const lastFrameTimeRef = (0, import_react.useRef)(0);
118
- const lastMouseYRef = (0, import_react.useRef)(0);
119
- const lastTouchYRef = (0, import_react.useRef)(0);
120
- const loopPointRef = (0, import_react.useRef)(0);
121
- const onPauseRef = (0, import_react.useRef)(onPause);
122
- const onResumeRef = (0, import_react.useRef)(onResume);
123
- const onScrollRef = (0, import_react.useRef)(onScroll);
124
- const positionRef = (0, import_react.useRef)(0);
125
- const resizeDebounceTimerRef = (0, import_react.useRef)(null);
126
- const velocityRef = (0, import_react.useRef)(0);
127
- (0, import_react.useEffect)(() => {
128
- onPauseRef.current = onPause;
129
- onResumeRef.current = onResume;
130
- onScrollRef.current = onScroll;
131
- }, [onPause, onResume, onScroll]);
132
- (0, import_react.useEffect)(() => {
133
- if (typeof window === "undefined") return;
134
- const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
135
- const handleChange = (e) => {
136
- setPrefersReducedMotion(e.matches);
137
- };
138
- mediaQuery.addEventListener("change", handleChange);
139
- return () => {
140
- mediaQuery.removeEventListener("change", handleChange);
141
- };
142
- }, []);
143
- const clearInactivityTimer = (0, import_react.useCallback)(() => {
144
- if (!inactivityTimerRef.current) return;
145
- clearTimeout(inactivityTimerRef.current);
146
- inactivityTimerRef.current = null;
147
- }, []);
148
- const renderPosition = (0, import_react.useCallback)((rawPosition) => {
149
- if (!containerRef.current) return;
150
- const currentLoopPoint = loopPointRef.current;
151
- let visualPosition = rawPosition % (currentLoopPoint || 1);
152
- if (visualPosition > 0 && currentLoopPoint > 0) {
153
- visualPosition -= currentLoopPoint;
154
- }
155
- containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
156
- }, []);
157
- const commitPosition = (0, import_react.useCallback)((nextPosition) => {
158
- if (positionRef.current === nextPosition) return;
159
- positionRef.current = nextPosition;
160
- renderPosition(nextPosition);
161
- onScrollRef.current?.(nextPosition);
162
- }, [renderPosition]);
163
- const wrapPosition = (0, import_react.useCallback)((rawPosition) => {
164
- const currentLoopPoint = loopPointRef.current;
165
- if (currentLoopPoint <= 0) return rawPosition;
166
- let wrappedPosition = rawPosition;
167
- while (wrappedPosition > 0) {
168
- wrappedPosition -= currentLoopPoint;
169
- }
170
- while (wrappedPosition < -currentLoopPoint * 2) {
171
- wrappedPosition += currentLoopPoint;
172
- }
173
- return wrappedPosition;
174
- }, []);
175
- const recalculateLoopPoint = (0, import_react.useCallback)(() => {
176
- if (!containerRef.current) return;
177
- const currentContentHeight = containerRef.current.scrollHeight;
178
- if (currentContentHeight <= 0) return;
179
- const nextLoopPoint = currentContentHeight / LOOP_SEGMENTS;
180
- loopPointRef.current = nextLoopPoint;
181
- setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < 0.5 ? previousLoopPoint : nextLoopPoint);
182
- renderPosition(positionRef.current);
183
- }, [renderPosition]);
184
- (0, import_react.useEffect)(() => {
185
- autoScrollRef.current.desired = autoScroll;
186
- if (!autoScroll) {
187
- clearInactivityTimer();
188
- autoScrollRef.current.active = true;
189
- }
190
- }, [autoScroll, clearInactivityTimer]);
191
- (0, import_react.useEffect)(() => {
192
- loopPointRef.current = loopPoint;
193
- renderPosition(positionRef.current);
194
- }, [loopPoint, renderPosition]);
195
- (0, import_react.useEffect)(() => {
196
- const rafId = requestAnimationFrame(recalculateLoopPoint);
197
- return () => {
198
- cancelAnimationFrame(rafId);
199
- };
200
- }, [children, recalculateLoopPoint]);
201
- const pauseAutoScroll = (0, import_react.useCallback)(() => {
202
- if (!pauseOnInteraction) return;
203
- autoScrollRef.current.active = false;
204
- onPauseRef.current?.();
205
- clearInactivityTimer();
206
- }, [clearInactivityTimer, pauseOnInteraction]);
207
- const scheduleAutoScrollResume = (0, import_react.useCallback)(() => {
208
- if (!pauseOnInteraction || !autoScrollRef.current.desired || isPausedRef.current) return;
209
- clearInactivityTimer();
210
- inactivityTimerRef.current = setTimeout(() => {
211
- inactivityTimerRef.current = null;
212
- if (!autoScrollRef.current.desired || isPausedRef.current) return;
213
- autoScrollRef.current.active = true;
214
- onResumeRef.current?.();
215
- }, normalizedResumeDelay);
216
- }, [clearInactivityTimer, normalizedResumeDelay, pauseOnInteraction]);
217
- const togglePause = (0, import_react.useCallback)(() => {
218
- const newPaused = !isPausedRef.current;
219
- isPausedRef.current = newPaused;
220
- setIsPaused(newPaused);
221
- if (newPaused) {
222
- clearInactivityTimer();
223
- autoScrollRef.current.active = false;
224
- onPauseRef.current?.();
225
- } else {
226
- clearInactivityTimer();
227
- autoScrollRef.current.active = true;
228
- if (autoScrollRef.current.desired) {
229
- onResumeRef.current?.();
230
- }
231
- }
232
- }, [clearInactivityTimer]);
233
- const handleKeyDown = (0, import_react.useCallback)((e) => {
234
- if (!keyboard) return;
235
- switch (e.key) {
236
- case " ":
237
- e.preventDefault();
238
- togglePause();
239
- break;
240
- case "ArrowDown":
241
- e.preventDefault();
242
- velocityRef.current -= ARROW_KEY_VELOCITY;
243
- pauseAutoScroll();
244
- scheduleAutoScrollResume();
245
- break;
246
- case "ArrowUp":
247
- e.preventDefault();
248
- velocityRef.current += ARROW_KEY_VELOCITY;
249
- pauseAutoScroll();
250
- scheduleAutoScrollResume();
251
- break;
252
- case "End":
253
- e.preventDefault();
254
- if (loopPointRef.current > 0) {
255
- commitPosition(-loopPointRef.current);
256
- }
257
- velocityRef.current = 0;
258
- pauseAutoScroll();
259
- scheduleAutoScrollResume();
260
- break;
261
- case "Home":
262
- e.preventDefault();
263
- commitPosition(0);
264
- velocityRef.current = 0;
265
- pauseAutoScroll();
266
- scheduleAutoScrollResume();
267
- break;
268
- default:
269
- break;
270
- }
271
- }, [commitPosition, keyboard, pauseAutoScroll, scheduleAutoScrollResume, togglePause]);
272
- const handleMouseDown = (0, import_react.useCallback)((e) => {
273
- if (!draggable) return;
274
- e.preventDefault();
275
- containerRef.current?.focus();
276
- setIsDragging(true);
277
- isDraggingRef.current = true;
278
- lastMouseYRef.current = e.clientY;
279
- velocityRef.current = 0;
280
- pauseAutoScroll();
281
- }, [draggable, pauseAutoScroll]);
282
- const handleMouseMove = (0, import_react.useCallback)((e) => {
283
- if (!isDraggingRef.current) return;
284
- e.preventDefault();
285
- const deltaY = e.clientY - lastMouseYRef.current;
286
- const nextPosition = wrapPosition(positionRef.current + deltaY);
287
- commitPosition(nextPosition);
288
- velocityRef.current = deltaY;
289
- lastMouseYRef.current = e.clientY;
290
- }, [commitPosition, wrapPosition]);
291
- const handleMouseUp = (0, import_react.useCallback)((e) => {
292
- if (!isDraggingRef.current) return;
293
- e.preventDefault();
294
- setIsDragging(false);
295
- isDraggingRef.current = false;
296
- scheduleAutoScrollResume();
297
- }, [scheduleAutoScrollResume]);
298
- const handleTouchEnd = (0, import_react.useCallback)((_e) => {
299
- if (!isDraggingRef.current) return;
300
- setIsDragging(false);
301
- isDraggingRef.current = false;
302
- scheduleAutoScrollResume();
303
- }, [scheduleAutoScrollResume]);
304
- const handleTouchMove = (0, import_react.useCallback)((e) => {
305
- if (!isDraggingRef.current || e.touches.length !== 1) return;
306
- e.preventDefault();
307
- const touch = e.touches[0];
308
- const deltaY = touch.clientY - lastTouchYRef.current;
309
- const nextPosition = wrapPosition(positionRef.current + deltaY);
310
- commitPosition(nextPosition);
311
- velocityRef.current = deltaY;
312
- lastTouchYRef.current = touch.clientY;
313
- }, [commitPosition, wrapPosition]);
314
- const handleTouchStart = (0, import_react.useCallback)((e) => {
315
- if (!draggable || e.touches.length !== 1) return;
316
- containerRef.current?.focus();
317
- setIsDragging(true);
318
- isDraggingRef.current = true;
319
- lastTouchYRef.current = e.touches[0].clientY;
320
- velocityRef.current = 0;
321
- pauseAutoScroll();
322
- }, [draggable, pauseAutoScroll]);
323
- const handleWheel = (0, import_react.useCallback)((e) => {
324
- if (!wheelEnabled) return;
325
- const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
326
- if (!currentContainer) return;
327
- e.preventDefault();
328
- const normalizedDeltaY = normalizeWheelDeltaY(e, currentContainer);
329
- const wheelDelta = -normalizedDeltaY;
330
- const nextPosition = wrapPosition(positionRef.current + wheelDelta * WHEEL_IMMEDIATE_DELTA_FACTOR);
331
- commitPosition(nextPosition);
332
- velocityRef.current += wheelDelta * WHEEL_VELOCITY_MULTIPLIER;
333
- velocityRef.current = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, velocityRef.current));
334
- pauseAutoScroll();
335
- scheduleAutoScrollResume();
336
- }, [commitPosition, pauseAutoScroll, scheduleAutoScrollResume, wheelEnabled, wrapPosition]);
337
- (0, import_react.useEffect)(() => {
338
- const currentContainer = containerRef.current;
339
- if (!currentContainer) return;
340
- currentContainer.addEventListener("mousedown", handleMouseDown);
341
- window.addEventListener("mousemove", handleMouseMove);
342
- window.addEventListener("mouseup", handleMouseUp);
343
- currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
344
- window.addEventListener("touchmove", handleTouchMove, { passive: false });
345
- window.addEventListener("touchend", handleTouchEnd, { passive: true });
346
- currentContainer.addEventListener("wheel", handleWheel, { passive: false });
347
- return () => {
348
- currentContainer.removeEventListener("mousedown", handleMouseDown);
349
- window.removeEventListener("mousemove", handleMouseMove);
350
- window.removeEventListener("mouseup", handleMouseUp);
351
- currentContainer.removeEventListener("touchstart", handleTouchStart);
352
- window.removeEventListener("touchmove", handleTouchMove);
353
- window.removeEventListener("touchend", handleTouchEnd);
354
- currentContainer.removeEventListener("wheel", handleWheel);
355
- };
356
- }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchEnd, handleTouchMove, handleTouchStart, handleWheel]);
357
- const debouncedRecalculate = (0, import_react.useCallback)(() => {
358
- if (resizeDebounceTimerRef.current) {
359
- clearTimeout(resizeDebounceTimerRef.current);
360
- }
361
- resizeDebounceTimerRef.current = setTimeout(() => {
362
- resizeDebounceTimerRef.current = null;
363
- recalculateLoopPoint();
364
- }, RESIZE_DEBOUNCE_MS);
365
- }, [recalculateLoopPoint]);
366
- (0, import_react.useEffect)(() => {
367
- window.addEventListener("resize", debouncedRecalculate);
368
- return () => {
369
- window.removeEventListener("resize", debouncedRecalculate);
370
- };
371
- }, [debouncedRecalculate]);
372
- (0, import_react.useEffect)(() => {
373
- if (!containerRef.current || typeof ResizeObserver === "undefined") return;
374
- const observer = new ResizeObserver(() => {
375
- debouncedRecalculate();
376
- });
377
- observer.observe(containerRef.current);
378
- return () => {
379
- observer.disconnect();
380
- };
381
- }, [debouncedRecalculate]);
382
- (0, import_react.useEffect)(() => {
383
- return () => {
384
- if (resizeDebounceTimerRef.current) {
385
- clearTimeout(resizeDebounceTimerRef.current);
386
- resizeDebounceTimerRef.current = null;
387
- }
388
- };
389
- }, []);
390
- (0, import_react.useEffect)(() => {
391
- const handleVisibilityChange = () => {
392
- setIsTabActive(!document.hidden);
393
- if (!document.hidden) {
394
- lastFrameTimeRef.current = performance.now();
395
- }
396
- };
397
- document.addEventListener("visibilitychange", handleVisibilityChange);
398
- return () => {
399
- document.removeEventListener("visibilitychange", handleVisibilityChange);
400
- };
401
- }, []);
402
- (0, import_react.useEffect)(() => {
403
- return () => {
404
- clearInactivityTimer();
405
- };
406
- }, [clearInactivityTimer]);
407
- (0, import_react.useEffect)(() => {
408
- if (!isTabActive || isPaused) {
409
- if (animationFrameRef.current) {
410
- cancelAnimationFrame(animationFrameRef.current);
411
- animationFrameRef.current = null;
412
- }
413
- return;
414
- }
415
- lastFrameTimeRef.current = 0;
416
- const directionMultiplier = direction === "down" ? 1 : -1;
417
- const animate = (currentTime) => {
418
- let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
419
- deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
420
- lastFrameTimeRef.current = currentTime;
421
- if (prefersReducedMotion) {
422
- velocityRef.current = 0;
423
- } else if (Math.abs(velocityRef.current) > 0.1) {
424
- velocityRef.current *= Math.pow(normalizedFriction, deltaTime);
425
- } else {
426
- velocityRef.current = 0;
427
- }
428
- let nextPosition = positionRef.current;
429
- const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
430
- if (autoScrollRef.current.desired && autoScrollRef.current.active && !isDraggingRef.current) {
431
- nextPosition += directionMultiplier * effectiveSpeed * deltaTime;
432
- }
433
- if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
434
- nextPosition += velocityRef.current * deltaTime;
435
- }
436
- nextPosition = wrapPosition(nextPosition);
437
- commitPosition(nextPosition);
438
- animationFrameRef.current = requestAnimationFrame(animate);
439
- };
440
- animationFrameRef.current = requestAnimationFrame(animate);
441
- return () => {
442
- if (animationFrameRef.current) {
443
- cancelAnimationFrame(animationFrameRef.current);
444
- }
445
- };
446
- }, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
447
- (0, import_react.useEffect)(() => {
448
- if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
449
- const observer = new IntersectionObserver(
450
- (entries) => {
451
- entries.forEach((entry) => {
452
- if (entry.isIntersecting) {
453
- entry.target.classList.add("visible");
454
- }
455
- });
456
- },
457
- {
458
- root: null,
459
- rootMargin: lazyRootMargin,
460
- threshold: lazyThreshold
461
- }
462
- );
463
- const items = containerRef.current.querySelectorAll(".content-item");
464
- items.forEach((item) => observer.observe(item));
465
- return () => {
466
- items.forEach((item) => observer.unobserve(item));
467
- observer.disconnect();
468
- };
469
- }, [children, lazy, lazyRootMargin, lazyThreshold]);
470
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
471
- "div",
472
- {
473
- className: "react-sway-container scroller-content",
474
- ref: containerRef,
475
- style: {
476
- cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
477
- MozUserSelect: "none",
478
- msUserSelect: "none",
479
- overflow: "hidden",
480
- overscrollBehavior: "contain",
481
- pointerEvents: "auto",
482
- position: "absolute",
483
- touchAction: "none",
484
- transform: "translate3d(0, 0px, 0)",
485
- userSelect: "none",
486
- WebkitUserSelect: "none",
487
- width: "100%",
488
- willChange: "transform",
489
- zIndex: 1
490
- },
491
- onKeyDown: keyboard ? handleKeyDown : void 0,
492
- tabIndex: keyboard ? 0 : void 0,
493
- children: [
494
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "content-group original", children }),
495
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children }),
496
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children })
497
- ]
498
- }
499
- );
72
+ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
73
+ const deltaX = normalizeWheelDelta(event, container, "x");
74
+ const deltaY = normalizeWheelDelta(event, container, "y");
75
+ const absDeltaX = Math.abs(deltaX);
76
+ const absDeltaY = Math.abs(deltaY);
77
+ if (isHorizontal) {
78
+ if (wheelMode === "axis" && absDeltaX <= absDeltaY && !event.shiftKey) return null;
79
+ return absDeltaX > absDeltaY ? deltaX : deltaY;
80
+ }
81
+ if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
82
+ return deltaY;
500
83
  }
501
- var ReactSway_default = ReactSway;
502
- // Annotate the CommonJS export names for ESM import in node:
503
- 0 && (module.exports = {
504
- ReactSway
505
- });
84
+ function getVisibleEdgeInterval(rect, isHorizontal) {
85
+ const rectEnd = isHorizontal ? rect.right : rect.bottom;
86
+ const rectStart = isHorizontal ? rect.left : rect.top;
87
+ const viewportEnd = isHorizontal ? window.innerWidth : window.innerHeight;
88
+ const end = Math.min(rectEnd, viewportEnd);
89
+ const start = Math.max(rectStart, 0);
90
+ if (end <= start) return null;
91
+ return {
92
+ end,
93
+ size: end - start,
94
+ start
95
+ };
96
+ }
97
+ /**
98
+ * A smooth, infinite scrolling container component.
99
+ *
100
+ * Renders children in a continuously looping scroll area with support for
101
+ * auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
102
+ * and optional edge-hover scrolling.
103
+ * Content is duplicated to create a seamless loop effect. Duplicate content
104
+ * is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
105
+ *
106
+ * Respects `prefers-reduced-motion: reduce` by lowering auto-scroll speed
107
+ * and disabling momentum effects.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <ReactSway direction="up" speed={1} friction={0.9}>
112
+ * <div className="content-item">Item 1</div>
113
+ * <div className="content-item">Item 2</div>
114
+ * </ReactSway>
115
+ * ```
116
+ */
117
+ function ReactSway({ axis, autoScroll = true, children, direction = "up", draggable = true, edgeHoverScroll = false, edgeHoverSize = DEFAULT_EDGE_HOVER_SIZE, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }) {
118
+ const isHorizontal = (0, react.useMemo)(() => axis ?? (direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]) === "horizontal";
119
+ const normalizedEdgeHoverSize = (0, react.useMemo)(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : DEFAULT_EDGE_HOVER_SIZE, [edgeHoverSize]);
120
+ const normalizedFriction = (0, react.useMemo)(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
121
+ const normalizedResumeDelay = (0, react.useMemo)(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
122
+ const normalizedSpeed = (0, react.useMemo)(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
123
+ const touchAction = (0, react.useMemo)(() => {
124
+ if (!draggable) return "auto";
125
+ return isHorizontal ? "pan-y" : "pan-x";
126
+ }, [draggable, isHorizontal]);
127
+ const overscrollBehavior = wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
128
+ const [isDragging, setIsDragging] = (0, react.useState)(false);
129
+ const [isPaused, setIsPaused] = (0, react.useState)(false);
130
+ const [isTabActive, setIsTabActive] = (0, react.useState)(true);
131
+ const [loopPoint, setLoopPoint] = (0, react.useState)(0);
132
+ const [loopSegmentCount, setLoopSegmentCount] = (0, react.useState)(MIN_LOOP_SEGMENTS);
133
+ const [prefersReducedMotion, setPrefersReducedMotion] = (0, react.useState)(() => {
134
+ if (typeof window === "undefined") return false;
135
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
136
+ });
137
+ const animationFrameRef = (0, react.useRef)(null);
138
+ const autoScrollRef = (0, react.useRef)({
139
+ active: autoScroll,
140
+ desired: autoScroll
141
+ });
142
+ const containerRef = (0, react.useRef)(null);
143
+ const inactivityTimerRef = (0, react.useRef)(null);
144
+ const isDraggingRef = (0, react.useRef)(false);
145
+ const isPausedRef = (0, react.useRef)(false);
146
+ const lastFrameTimeRef = (0, react.useRef)(0);
147
+ const lastMouseXRef = (0, react.useRef)(0);
148
+ const lastMouseYRef = (0, react.useRef)(0);
149
+ const lastTouchXRef = (0, react.useRef)(0);
150
+ const lastTouchYRef = (0, react.useRef)(0);
151
+ const loopPointRef = (0, react.useRef)(0);
152
+ const edgeHoverDirectionRef = (0, react.useRef)(0);
153
+ const onPauseRef = (0, react.useRef)(onPause);
154
+ const onResumeRef = (0, react.useRef)(onResume);
155
+ const onScrollRef = (0, react.useRef)(onScroll);
156
+ const originalGroupRef = (0, react.useRef)(null);
157
+ const positionRef = (0, react.useRef)(0);
158
+ const resizeDebounceTimerRef = (0, react.useRef)(null);
159
+ const touchInteractionStateRef = (0, react.useRef)("idle");
160
+ const touchStartXRef = (0, react.useRef)(0);
161
+ const touchStartYRef = (0, react.useRef)(0);
162
+ const velocityRef = (0, react.useRef)(0);
163
+ (0, react.useEffect)(() => {
164
+ onPauseRef.current = onPause;
165
+ onResumeRef.current = onResume;
166
+ onScrollRef.current = onScroll;
167
+ }, [
168
+ onPause,
169
+ onResume,
170
+ onScroll
171
+ ]);
172
+ (0, react.useEffect)(() => {
173
+ if (typeof window === "undefined") return;
174
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
175
+ const handleChange = (e) => {
176
+ setPrefersReducedMotion(e.matches);
177
+ };
178
+ mediaQuery.addEventListener("change", handleChange);
179
+ return () => {
180
+ mediaQuery.removeEventListener("change", handleChange);
181
+ };
182
+ }, []);
183
+ const clearInactivityTimer = (0, react.useCallback)(() => {
184
+ if (!inactivityTimerRef.current) return;
185
+ clearTimeout(inactivityTimerRef.current);
186
+ inactivityTimerRef.current = null;
187
+ }, []);
188
+ const renderPosition = (0, react.useCallback)((rawPosition) => {
189
+ if (!containerRef.current) return;
190
+ const currentLoopPoint = loopPointRef.current;
191
+ let visualPosition = rawPosition % (currentLoopPoint || 1);
192
+ if (visualPosition > 0 && currentLoopPoint > 0) visualPosition -= currentLoopPoint;
193
+ containerRef.current.style.transform = isHorizontal ? `translate3d(${visualPosition}px, 0, 0)` : `translate3d(0, ${visualPosition}px, 0)`;
194
+ }, [isHorizontal]);
195
+ const commitPosition = (0, react.useCallback)((nextPosition) => {
196
+ if (positionRef.current === nextPosition) return;
197
+ positionRef.current = nextPosition;
198
+ renderPosition(nextPosition);
199
+ onScrollRef.current?.(nextPosition);
200
+ }, [renderPosition]);
201
+ const wrapPosition = (0, react.useCallback)((rawPosition) => {
202
+ const currentLoopPoint = loopPointRef.current;
203
+ if (currentLoopPoint <= 0) return rawPosition;
204
+ let wrappedPosition = rawPosition;
205
+ while (wrappedPosition > 0) wrappedPosition -= currentLoopPoint;
206
+ while (wrappedPosition < -currentLoopPoint * 2) wrappedPosition += currentLoopPoint;
207
+ return wrappedPosition;
208
+ }, []);
209
+ const recalculateLoopPoint = (0, react.useCallback)(() => {
210
+ if (!containerRef.current || !originalGroupRef.current) return;
211
+ const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
212
+ if (originalContentSize <= 0) return;
213
+ const viewport = containerRef.current.parentElement ?? containerRef.current;
214
+ const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
215
+ const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 2);
216
+ const nextLoopPoint = originalContentSize;
217
+ setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
218
+ loopPointRef.current = nextLoopPoint;
219
+ setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < .5 ? previousLoopPoint : nextLoopPoint);
220
+ renderPosition(positionRef.current);
221
+ }, [isHorizontal, renderPosition]);
222
+ (0, react.useEffect)(() => {
223
+ autoScrollRef.current.desired = autoScroll;
224
+ if (!autoScroll) {
225
+ clearInactivityTimer();
226
+ autoScrollRef.current.active = true;
227
+ }
228
+ }, [autoScroll, clearInactivityTimer]);
229
+ (0, react.useEffect)(() => {
230
+ loopPointRef.current = loopPoint;
231
+ renderPosition(positionRef.current);
232
+ }, [loopPoint, renderPosition]);
233
+ (0, react.useEffect)(() => {
234
+ const rafId = requestAnimationFrame(recalculateLoopPoint);
235
+ return () => {
236
+ cancelAnimationFrame(rafId);
237
+ };
238
+ }, [children, recalculateLoopPoint]);
239
+ const pauseAutoScroll = (0, react.useCallback)(() => {
240
+ if (!pauseOnInteraction) return;
241
+ autoScrollRef.current.active = false;
242
+ onPauseRef.current?.();
243
+ clearInactivityTimer();
244
+ }, [clearInactivityTimer, pauseOnInteraction]);
245
+ const scheduleAutoScrollResume = (0, react.useCallback)(() => {
246
+ if (!pauseOnInteraction || !autoScrollRef.current.desired || isPausedRef.current) return;
247
+ clearInactivityTimer();
248
+ inactivityTimerRef.current = setTimeout(() => {
249
+ inactivityTimerRef.current = null;
250
+ if (!autoScrollRef.current.desired || isPausedRef.current) return;
251
+ autoScrollRef.current.active = true;
252
+ onResumeRef.current?.();
253
+ }, normalizedResumeDelay);
254
+ }, [
255
+ clearInactivityTimer,
256
+ normalizedResumeDelay,
257
+ pauseOnInteraction
258
+ ]);
259
+ const togglePause = (0, react.useCallback)(() => {
260
+ const newPaused = !isPausedRef.current;
261
+ isPausedRef.current = newPaused;
262
+ setIsPaused(newPaused);
263
+ if (newPaused) {
264
+ clearInactivityTimer();
265
+ autoScrollRef.current.active = false;
266
+ onPauseRef.current?.();
267
+ } else {
268
+ clearInactivityTimer();
269
+ autoScrollRef.current.active = true;
270
+ if (autoScrollRef.current.desired) onResumeRef.current?.();
271
+ }
272
+ }, [clearInactivityTimer]);
273
+ const handleKeyDown = (0, react.useCallback)((e) => {
274
+ if (!keyboard) return;
275
+ switch (e.key) {
276
+ case " ":
277
+ e.preventDefault();
278
+ togglePause();
279
+ break;
280
+ case "ArrowDown":
281
+ if (isHorizontal) break;
282
+ e.preventDefault();
283
+ velocityRef.current -= ARROW_KEY_VELOCITY;
284
+ pauseAutoScroll();
285
+ scheduleAutoScrollResume();
286
+ break;
287
+ case "ArrowUp":
288
+ if (isHorizontal) break;
289
+ e.preventDefault();
290
+ velocityRef.current += ARROW_KEY_VELOCITY;
291
+ pauseAutoScroll();
292
+ scheduleAutoScrollResume();
293
+ break;
294
+ case "ArrowLeft":
295
+ if (!isHorizontal) break;
296
+ e.preventDefault();
297
+ velocityRef.current += ARROW_KEY_VELOCITY;
298
+ pauseAutoScroll();
299
+ scheduleAutoScrollResume();
300
+ break;
301
+ case "ArrowRight":
302
+ if (!isHorizontal) break;
303
+ e.preventDefault();
304
+ velocityRef.current -= ARROW_KEY_VELOCITY;
305
+ pauseAutoScroll();
306
+ scheduleAutoScrollResume();
307
+ break;
308
+ case "End":
309
+ e.preventDefault();
310
+ if (loopPointRef.current > 0) commitPosition(-loopPointRef.current);
311
+ velocityRef.current = 0;
312
+ pauseAutoScroll();
313
+ scheduleAutoScrollResume();
314
+ break;
315
+ case "Home":
316
+ e.preventDefault();
317
+ commitPosition(0);
318
+ velocityRef.current = 0;
319
+ pauseAutoScroll();
320
+ scheduleAutoScrollResume();
321
+ break;
322
+ default: break;
323
+ }
324
+ }, [
325
+ commitPosition,
326
+ isHorizontal,
327
+ keyboard,
328
+ pauseAutoScroll,
329
+ scheduleAutoScrollResume,
330
+ togglePause
331
+ ]);
332
+ const handleMouseDown = (0, react.useCallback)((e) => {
333
+ if (!draggable) return;
334
+ e.preventDefault();
335
+ containerRef.current?.focus();
336
+ setIsDragging(true);
337
+ isDraggingRef.current = true;
338
+ lastMouseXRef.current = e.clientX;
339
+ lastMouseYRef.current = e.clientY;
340
+ velocityRef.current = 0;
341
+ pauseAutoScroll();
342
+ }, [draggable, pauseAutoScroll]);
343
+ const handleMouseMove = (0, react.useCallback)((e) => {
344
+ if (!isDraggingRef.current) return;
345
+ e.preventDefault();
346
+ const delta = isHorizontal ? e.clientX - lastMouseXRef.current : e.clientY - lastMouseYRef.current;
347
+ const nextPosition = wrapPosition(positionRef.current + delta);
348
+ commitPosition(nextPosition);
349
+ velocityRef.current = delta;
350
+ lastMouseXRef.current = e.clientX;
351
+ lastMouseYRef.current = e.clientY;
352
+ }, [
353
+ commitPosition,
354
+ isHorizontal,
355
+ wrapPosition
356
+ ]);
357
+ const handleMouseUp = (0, react.useCallback)((e) => {
358
+ if (!isDraggingRef.current) return;
359
+ e.preventDefault();
360
+ setIsDragging(false);
361
+ isDraggingRef.current = false;
362
+ scheduleAutoScrollResume();
363
+ }, [scheduleAutoScrollResume]);
364
+ const handleTouchEnd = (0, react.useCallback)((_e) => {
365
+ const wasActive = touchInteractionStateRef.current === "active";
366
+ touchInteractionStateRef.current = "idle";
367
+ if (isDraggingRef.current) {
368
+ setIsDragging(false);
369
+ isDraggingRef.current = false;
370
+ }
371
+ if (wasActive) scheduleAutoScrollResume();
372
+ }, [scheduleAutoScrollResume]);
373
+ const handleTouchMove = (0, react.useCallback)((e) => {
374
+ if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
375
+ if (e.touches.length !== 1) return;
376
+ const touch = e.touches[0];
377
+ if (touchInteractionStateRef.current === "pending") {
378
+ const totalDeltaX = touch.clientX - touchStartXRef.current;
379
+ const totalDeltaY = touch.clientY - touchStartYRef.current;
380
+ const primaryDelta = isHorizontal ? Math.abs(totalDeltaX) : Math.abs(totalDeltaY);
381
+ const crossDelta = isHorizontal ? Math.abs(totalDeltaY) : Math.abs(totalDeltaX);
382
+ if (Math.max(primaryDelta, crossDelta) < TOUCH_AXIS_LOCK_THRESHOLD_PX) return;
383
+ if (crossDelta > primaryDelta) {
384
+ touchInteractionStateRef.current = "ignored";
385
+ return;
386
+ }
387
+ touchInteractionStateRef.current = "active";
388
+ setIsDragging(true);
389
+ isDraggingRef.current = true;
390
+ pauseAutoScroll();
391
+ }
392
+ e.preventDefault();
393
+ const delta = isHorizontal ? touch.clientX - lastTouchXRef.current : touch.clientY - lastTouchYRef.current;
394
+ const nextPosition = wrapPosition(positionRef.current + delta);
395
+ commitPosition(nextPosition);
396
+ velocityRef.current = delta;
397
+ lastTouchXRef.current = touch.clientX;
398
+ lastTouchYRef.current = touch.clientY;
399
+ }, [
400
+ commitPosition,
401
+ isHorizontal,
402
+ pauseAutoScroll,
403
+ wrapPosition
404
+ ]);
405
+ const handleTouchStart = (0, react.useCallback)((e) => {
406
+ if (!draggable || e.touches.length !== 1) return;
407
+ containerRef.current?.focus();
408
+ touchInteractionStateRef.current = "pending";
409
+ lastTouchXRef.current = e.touches[0].clientX;
410
+ lastTouchYRef.current = e.touches[0].clientY;
411
+ touchStartXRef.current = e.touches[0].clientX;
412
+ touchStartYRef.current = e.touches[0].clientY;
413
+ velocityRef.current = 0;
414
+ }, [draggable]);
415
+ const handleWheel = (0, react.useCallback)((e) => {
416
+ if (!wheelEnabled) return;
417
+ const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
418
+ if (!currentContainer) return;
419
+ const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
420
+ if (ownedDelta === null) return;
421
+ e.preventDefault();
422
+ const wheelDelta = -ownedDelta;
423
+ const nextPosition = wrapPosition(positionRef.current + wheelDelta * WHEEL_IMMEDIATE_DELTA_FACTOR);
424
+ commitPosition(nextPosition);
425
+ velocityRef.current += wheelDelta * WHEEL_VELOCITY_MULTIPLIER;
426
+ velocityRef.current = Math.max(-150, Math.min(MAX_VELOCITY, velocityRef.current));
427
+ pauseAutoScroll();
428
+ scheduleAutoScrollResume();
429
+ }, [
430
+ commitPosition,
431
+ isHorizontal,
432
+ pauseAutoScroll,
433
+ scheduleAutoScrollResume,
434
+ wheelEnabled,
435
+ wheelMode,
436
+ wrapPosition
437
+ ]);
438
+ const handleEdgeHoverMove = (0, react.useCallback)((e) => {
439
+ if (!edgeHoverScroll) return;
440
+ const viewport = containerRef.current?.parentElement ?? containerRef.current;
441
+ if (!viewport) return;
442
+ const visibleInterval = getVisibleEdgeInterval(viewport.getBoundingClientRect(), isHorizontal);
443
+ if (!visibleInterval) {
444
+ edgeHoverDirectionRef.current = 0;
445
+ return;
446
+ }
447
+ const pointerPosition = isHorizontal ? e.clientX : e.clientY;
448
+ const effectiveEdgeHoverSize = Math.min(normalizedEdgeHoverSize, visibleInterval.size / 2);
449
+ if (pointerPosition <= visibleInterval.start + effectiveEdgeHoverSize) {
450
+ edgeHoverDirectionRef.current = 1;
451
+ return;
452
+ }
453
+ if (pointerPosition >= visibleInterval.end - effectiveEdgeHoverSize) {
454
+ edgeHoverDirectionRef.current = -1;
455
+ return;
456
+ }
457
+ edgeHoverDirectionRef.current = 0;
458
+ }, [
459
+ edgeHoverScroll,
460
+ isHorizontal,
461
+ normalizedEdgeHoverSize
462
+ ]);
463
+ const handleEdgeHoverLeave = (0, react.useCallback)(() => {
464
+ edgeHoverDirectionRef.current = 0;
465
+ }, []);
466
+ (0, react.useEffect)(() => {
467
+ const currentContainer = containerRef.current;
468
+ if (!currentContainer) return;
469
+ currentContainer.addEventListener("mousedown", handleMouseDown);
470
+ window.addEventListener("mousemove", handleMouseMove);
471
+ window.addEventListener("mouseup", handleMouseUp);
472
+ if (draggable) {
473
+ currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
474
+ window.addEventListener("touchmove", handleTouchMove, { passive: false });
475
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
476
+ }
477
+ if (wheelEnabled) currentContainer.addEventListener("wheel", handleWheel, { passive: false });
478
+ const edgeHoverTarget = currentContainer.parentElement ?? currentContainer;
479
+ edgeHoverTarget.addEventListener("mousemove", handleEdgeHoverMove);
480
+ edgeHoverTarget.addEventListener("mouseleave", handleEdgeHoverLeave);
481
+ return () => {
482
+ currentContainer.removeEventListener("mousedown", handleMouseDown);
483
+ window.removeEventListener("mousemove", handleMouseMove);
484
+ window.removeEventListener("mouseup", handleMouseUp);
485
+ if (draggable) {
486
+ currentContainer.removeEventListener("touchstart", handleTouchStart);
487
+ window.removeEventListener("touchmove", handleTouchMove);
488
+ window.removeEventListener("touchend", handleTouchEnd);
489
+ }
490
+ if (wheelEnabled) currentContainer.removeEventListener("wheel", handleWheel);
491
+ edgeHoverTarget.removeEventListener("mousemove", handleEdgeHoverMove);
492
+ edgeHoverTarget.removeEventListener("mouseleave", handleEdgeHoverLeave);
493
+ };
494
+ }, [
495
+ draggable,
496
+ handleEdgeHoverLeave,
497
+ handleEdgeHoverMove,
498
+ handleMouseDown,
499
+ handleMouseMove,
500
+ handleMouseUp,
501
+ handleTouchEnd,
502
+ handleTouchMove,
503
+ handleTouchStart,
504
+ handleWheel,
505
+ wheelEnabled
506
+ ]);
507
+ const debouncedRecalculate = (0, react.useCallback)(() => {
508
+ if (resizeDebounceTimerRef.current) clearTimeout(resizeDebounceTimerRef.current);
509
+ resizeDebounceTimerRef.current = setTimeout(() => {
510
+ resizeDebounceTimerRef.current = null;
511
+ recalculateLoopPoint();
512
+ }, RESIZE_DEBOUNCE_MS);
513
+ }, [recalculateLoopPoint]);
514
+ (0, react.useEffect)(() => {
515
+ window.addEventListener("resize", debouncedRecalculate);
516
+ return () => {
517
+ window.removeEventListener("resize", debouncedRecalculate);
518
+ };
519
+ }, [debouncedRecalculate]);
520
+ (0, react.useEffect)(() => {
521
+ if (!containerRef.current || typeof ResizeObserver === "undefined") return;
522
+ const observer = new ResizeObserver(() => {
523
+ debouncedRecalculate();
524
+ });
525
+ observer.observe(containerRef.current);
526
+ return () => {
527
+ observer.disconnect();
528
+ };
529
+ }, [debouncedRecalculate]);
530
+ (0, react.useEffect)(() => {
531
+ return () => {
532
+ if (resizeDebounceTimerRef.current) {
533
+ clearTimeout(resizeDebounceTimerRef.current);
534
+ resizeDebounceTimerRef.current = null;
535
+ }
536
+ };
537
+ }, []);
538
+ (0, react.useEffect)(() => {
539
+ const handleVisibilityChange = () => {
540
+ setIsTabActive(!document.hidden);
541
+ if (!document.hidden) lastFrameTimeRef.current = performance.now();
542
+ };
543
+ document.addEventListener("visibilitychange", handleVisibilityChange);
544
+ return () => {
545
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
546
+ };
547
+ }, []);
548
+ (0, react.useEffect)(() => {
549
+ return () => {
550
+ clearInactivityTimer();
551
+ };
552
+ }, [clearInactivityTimer]);
553
+ (0, react.useEffect)(() => {
554
+ if (!isTabActive || isPaused) {
555
+ if (animationFrameRef.current) {
556
+ cancelAnimationFrame(animationFrameRef.current);
557
+ animationFrameRef.current = null;
558
+ }
559
+ return;
560
+ }
561
+ lastFrameTimeRef.current = 0;
562
+ const directionMultiplier = direction === "down" || direction === "right" ? 1 : -1;
563
+ const animate = (currentTime) => {
564
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
565
+ deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
566
+ lastFrameTimeRef.current = currentTime;
567
+ if (prefersReducedMotion) velocityRef.current = 0;
568
+ else if (Math.abs(velocityRef.current) > .1) velocityRef.current *= Math.pow(normalizedFriction, deltaTime);
569
+ else velocityRef.current = 0;
570
+ let nextPosition = positionRef.current;
571
+ const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
572
+ const edgeHoverMultiplier = edgeHoverScroll ? edgeHoverDirectionRef.current : directionMultiplier;
573
+ if (autoScrollRef.current.desired && autoScrollRef.current.active && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0)) nextPosition += edgeHoverMultiplier * effectiveSpeed * deltaTime;
574
+ if (!isDraggingRef.current && Math.abs(velocityRef.current) > .1) nextPosition += velocityRef.current * deltaTime;
575
+ nextPosition = wrapPosition(nextPosition);
576
+ commitPosition(nextPosition);
577
+ animationFrameRef.current = requestAnimationFrame(animate);
578
+ };
579
+ animationFrameRef.current = requestAnimationFrame(animate);
580
+ return () => {
581
+ if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
582
+ };
583
+ }, [
584
+ commitPosition,
585
+ direction,
586
+ edgeHoverScroll,
587
+ isHorizontal,
588
+ isPaused,
589
+ isTabActive,
590
+ normalizedFriction,
591
+ normalizedSpeed,
592
+ prefersReducedMotion,
593
+ wrapPosition
594
+ ]);
595
+ (0, react.useEffect)(() => {
596
+ if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
597
+ const observer = new IntersectionObserver((entries) => {
598
+ entries.forEach((entry) => {
599
+ if (entry.isIntersecting) entry.target.classList.add("visible");
600
+ });
601
+ }, {
602
+ root: null,
603
+ rootMargin: lazyRootMargin,
604
+ threshold: lazyThreshold
605
+ });
606
+ const items = containerRef.current.querySelectorAll(".content-item");
607
+ items.forEach((item) => observer.observe(item));
608
+ return () => {
609
+ items.forEach((item) => observer.unobserve(item));
610
+ observer.disconnect();
611
+ };
612
+ }, [
613
+ children,
614
+ lazy,
615
+ lazyRootMargin,
616
+ lazyThreshold
617
+ ]);
618
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
619
+ className: "react-sway-container scroller-content",
620
+ ref: containerRef,
621
+ style: {
622
+ cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
623
+ display: isHorizontal ? "flex" : void 0,
624
+ flexDirection: isHorizontal ? "row" : void 0,
625
+ height: isHorizontal ? "100%" : "max-content",
626
+ MozUserSelect: "none",
627
+ msUserSelect: "none",
628
+ minHeight: isHorizontal ? void 0 : "100%",
629
+ overflow: "hidden",
630
+ overscrollBehavior,
631
+ pointerEvents: "auto",
632
+ position: "absolute",
633
+ touchAction,
634
+ transform: "translate3d(0, 0, 0)",
635
+ userSelect: "none",
636
+ WebkitUserSelect: "none",
637
+ width: isHorizontal ? "max-content" : "100%",
638
+ willChange: "transform",
639
+ zIndex: 1
640
+ },
641
+ onKeyDown: keyboard ? handleKeyDown : void 0,
642
+ tabIndex: keyboard ? 0 : void 0,
643
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
644
+ className: "content-group original",
645
+ ref: originalGroupRef,
646
+ style: isHorizontal ? {
647
+ display: "flex",
648
+ flex: "0 0 auto"
649
+ } : void 0,
650
+ children
651
+ }), Array.from({ length: loopSegmentCount - 1 }, (_, duplicateIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
652
+ "aria-hidden": "true",
653
+ className: "content-group duplicate",
654
+ "data-duplicate": "true",
655
+ role: "presentation",
656
+ style: isHorizontal ? {
657
+ display: "flex",
658
+ flex: "0 0 auto"
659
+ } : void 0,
660
+ children
661
+ }, `duplicate-${duplicateIndex}`))]
662
+ });
663
+ }
664
+ //#endregion
665
+ exports.ReactSway = ReactSway;