react-sway 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -27,206 +27,330 @@ module.exports = __toCommonJS(index_exports);
27
27
  // src/ReactSway.tsx
28
28
  var import_react = require("react");
29
29
  var import_jsx_runtime = require("react/jsx-runtime");
30
- var SCROLL_SPEED = 0.5;
31
- var INACTIVITY_DELAY = 2e3;
32
- var FRICTION = 0.95;
30
+ var ARROW_KEY_VELOCITY = 15;
31
+ var DEFAULT_FRICTION = 0.95;
32
+ var DEFAULT_LAZY_ROOT_MARGIN = "100px";
33
+ var DEFAULT_LAZY_THRESHOLD = 0.01;
34
+ var DEFAULT_RESUME_DELAY = 2e3;
35
+ var DEFAULT_SPEED = 0.5;
36
+ var LOOP_SEGMENTS = 3;
33
37
  var MAX_DELTA_TIME = 3;
34
- function ReactSway({ children }) {
35
- const [position, setPosition] = (0, import_react.useState)(0);
36
- const [, setVelocity] = (0, import_react.useState)(0);
38
+ var MAX_VELOCITY = 150;
39
+ var MS_PER_FRAME_60FPS = 16.667;
40
+ var REDUCED_MOTION_SPEED_FACTOR = 0.25;
41
+ var RESIZE_DEBOUNCE_MS = 150;
42
+ var WHEEL_VELOCITY_MULTIPLIER = 0.3;
43
+ function ReactSway({
44
+ autoScroll = true,
45
+ children,
46
+ direction = "up",
47
+ draggable = true,
48
+ friction = DEFAULT_FRICTION,
49
+ keyboard = true,
50
+ lazy = true,
51
+ lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN,
52
+ lazyThreshold = DEFAULT_LAZY_THRESHOLD,
53
+ onPause,
54
+ onResume,
55
+ onScroll,
56
+ pauseOnInteraction = true,
57
+ resumeDelay = DEFAULT_RESUME_DELAY,
58
+ speed = DEFAULT_SPEED,
59
+ wheelEnabled = true
60
+ }) {
61
+ const normalizedFriction = (0, import_react.useMemo)(
62
+ () => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION,
63
+ [friction]
64
+ );
65
+ const normalizedResumeDelay = (0, import_react.useMemo)(
66
+ () => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY,
67
+ [resumeDelay]
68
+ );
69
+ const normalizedSpeed = (0, import_react.useMemo)(
70
+ () => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED,
71
+ [speed]
72
+ );
37
73
  const [isDragging, setIsDragging] = (0, import_react.useState)(false);
38
74
  const [isPaused, setIsPaused] = (0, import_react.useState)(false);
39
- const [autoScrollEnabled, setAutoScrollEnabled] = (0, import_react.useState)(true);
40
75
  const [isTabActive, setIsTabActive] = (0, import_react.useState)(true);
41
- const [contentHeight, setContentHeight] = (0, import_react.useState)(0);
42
76
  const [loopPoint, setLoopPoint] = (0, import_react.useState)(0);
43
- const [, setContainerHeight] = (0, import_react.useState)(0);
44
- const containerRef = (0, import_react.useRef)(null);
77
+ const [prefersReducedMotion, setPrefersReducedMotion] = (0, import_react.useState)(() => {
78
+ if (typeof window === "undefined") return false;
79
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
80
+ });
45
81
  const animationFrameRef = (0, import_react.useRef)(null);
82
+ const autoScrollRef = (0, import_react.useRef)({ active: autoScroll, desired: autoScroll });
83
+ const containerRef = (0, import_react.useRef)(null);
46
84
  const inactivityTimerRef = (0, import_react.useRef)(null);
47
- const lastTouchYRef = (0, import_react.useRef)(0);
48
- const lastMouseYRef = (0, import_react.useRef)(0);
85
+ const isDraggingRef = (0, import_react.useRef)(false);
86
+ const isPausedRef = (0, import_react.useRef)(false);
49
87
  const lastFrameTimeRef = (0, import_react.useRef)(0);
50
- let visualPosition = position % (loopPoint || 1);
51
- if (visualPosition > 0 && loopPoint > 0) visualPosition -= loopPoint;
88
+ const lastMouseYRef = (0, import_react.useRef)(0);
89
+ const lastTouchYRef = (0, import_react.useRef)(0);
90
+ const loopPointRef = (0, import_react.useRef)(0);
91
+ const onPauseRef = (0, import_react.useRef)(onPause);
92
+ const onResumeRef = (0, import_react.useRef)(onResume);
93
+ const onScrollRef = (0, import_react.useRef)(onScroll);
94
+ const positionRef = (0, import_react.useRef)(0);
95
+ const resizeDebounceTimerRef = (0, import_react.useRef)(null);
96
+ const velocityRef = (0, import_react.useRef)(0);
52
97
  (0, import_react.useEffect)(() => {
53
- const calculateDimensions = () => {
54
- if (containerRef.current) {
55
- containerRef.current.offsetHeight;
56
- const currentContentHeight = containerRef.current.scrollHeight;
57
- const calculatedLoopPoint = currentContentHeight / 3;
58
- console.log("Calculating dimensions:", { currentContentHeight, calculatedLoopPoint });
59
- if (currentContentHeight > 0) {
60
- setContentHeight(currentContentHeight);
61
- setLoopPoint(calculatedLoopPoint);
62
- }
63
- setContainerHeight(window.innerHeight);
64
- }
98
+ onPauseRef.current = onPause;
99
+ onResumeRef.current = onResume;
100
+ onScrollRef.current = onScroll;
101
+ }, [onPause, onResume, onScroll]);
102
+ (0, import_react.useEffect)(() => {
103
+ if (typeof window === "undefined") return;
104
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
105
+ const handleChange = (e) => {
106
+ setPrefersReducedMotion(e.matches);
65
107
  };
66
- const rafId = requestAnimationFrame(() => {
67
- calculateDimensions();
68
- });
108
+ mediaQuery.addEventListener("change", handleChange);
69
109
  return () => {
70
- cancelAnimationFrame(rafId);
110
+ mediaQuery.removeEventListener("change", handleChange);
71
111
  };
72
- }, [children]);
73
- const pauseAutoScroll = (0, import_react.useCallback)(() => {
74
- setAutoScrollEnabled(false);
75
- if (inactivityTimerRef.current) {
76
- clearTimeout(inactivityTimerRef.current);
112
+ }, []);
113
+ const clearInactivityTimer = (0, import_react.useCallback)(() => {
114
+ if (!inactivityTimerRef.current) return;
115
+ clearTimeout(inactivityTimerRef.current);
116
+ inactivityTimerRef.current = null;
117
+ }, []);
118
+ const renderPosition = (0, import_react.useCallback)((rawPosition) => {
119
+ if (!containerRef.current) return;
120
+ const currentLoopPoint = loopPointRef.current;
121
+ let visualPosition = rawPosition % (currentLoopPoint || 1);
122
+ if (visualPosition > 0 && currentLoopPoint > 0) {
123
+ visualPosition -= currentLoopPoint;
77
124
  }
125
+ containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
78
126
  }, []);
79
- const scheduleAutoScrollResume = (0, import_react.useCallback)(() => {
80
- if (inactivityTimerRef.current) {
81
- clearTimeout(inactivityTimerRef.current);
127
+ const commitPosition = (0, import_react.useCallback)((nextPosition) => {
128
+ if (positionRef.current === nextPosition) return;
129
+ positionRef.current = nextPosition;
130
+ renderPosition(nextPosition);
131
+ onScrollRef.current?.(nextPosition);
132
+ }, [renderPosition]);
133
+ const wrapPosition = (0, import_react.useCallback)((rawPosition) => {
134
+ const currentLoopPoint = loopPointRef.current;
135
+ if (currentLoopPoint <= 0) return rawPosition;
136
+ let wrappedPosition = rawPosition;
137
+ while (wrappedPosition > 0) {
138
+ wrappedPosition -= currentLoopPoint;
82
139
  }
83
- inactivityTimerRef.current = setTimeout(() => {
84
- setAutoScrollEnabled(true);
85
- }, INACTIVITY_DELAY);
140
+ while (wrappedPosition < -currentLoopPoint * 2) {
141
+ wrappedPosition += currentLoopPoint;
142
+ }
143
+ return wrappedPosition;
86
144
  }, []);
87
- const handleMouseDown = (0, import_react.useCallback)((e) => {
88
- e.preventDefault();
89
- setIsDragging(true);
90
- lastMouseYRef.current = e.clientY;
91
- setVelocity(0);
92
- pauseAutoScroll();
93
- }, [pauseAutoScroll]);
94
- const handleMouseMove = (0, import_react.useCallback)((e) => {
95
- if (!isDragging) return;
96
- e.preventDefault();
97
- const deltaY = e.clientY - lastMouseYRef.current;
98
- setPosition((prev) => prev + deltaY);
99
- setVelocity(deltaY);
100
- lastMouseYRef.current = e.clientY;
101
- }, [isDragging]);
102
- const handleMouseUp = (0, import_react.useCallback)((e) => {
103
- if (!isDragging) return;
104
- e.preventDefault();
105
- setIsDragging(false);
106
- scheduleAutoScrollResume();
107
- }, [isDragging, scheduleAutoScrollResume]);
108
- const handleTouchStart = (0, import_react.useCallback)((e) => {
109
- if (e.touches.length === 1) {
110
- setIsDragging(true);
111
- lastTouchYRef.current = e.touches[0].clientY;
112
- setVelocity(0);
113
- pauseAutoScroll();
145
+ const recalculateLoopPoint = (0, import_react.useCallback)(() => {
146
+ if (!containerRef.current) return;
147
+ const currentContentHeight = containerRef.current.scrollHeight;
148
+ if (currentContentHeight <= 0) return;
149
+ const nextLoopPoint = currentContentHeight / LOOP_SEGMENTS;
150
+ loopPointRef.current = nextLoopPoint;
151
+ setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < 0.5 ? previousLoopPoint : nextLoopPoint);
152
+ renderPosition(positionRef.current);
153
+ }, [renderPosition]);
154
+ (0, import_react.useEffect)(() => {
155
+ autoScrollRef.current.desired = autoScroll;
156
+ if (!autoScroll) {
157
+ clearInactivityTimer();
158
+ autoScrollRef.current.active = true;
114
159
  }
115
- }, [pauseAutoScroll]);
116
- const handleTouchMove = (0, import_react.useCallback)((e) => {
117
- if (!isDragging || e.touches.length !== 1) return;
118
- e.preventDefault();
119
- const touch = e.touches[0];
120
- const deltaY = touch.clientY - lastTouchYRef.current;
121
- setPosition((prev) => prev + deltaY);
122
- setVelocity(deltaY);
123
- lastTouchYRef.current = touch.clientY;
124
- }, [isDragging]);
125
- const handleTouchEnd = (0, import_react.useCallback)((_e) => {
126
- if (!isDragging) return;
127
- setIsDragging(false);
128
- scheduleAutoScrollResume();
129
- }, [isDragging, scheduleAutoScrollResume]);
130
- const handleWheel = (0, import_react.useCallback)((e) => {
131
- e.preventDefault();
132
- setVelocity((prev) => prev - e.deltaY * 0.3);
133
- pauseAutoScroll();
134
- scheduleAutoScrollResume();
135
- }, [pauseAutoScroll, scheduleAutoScrollResume]);
160
+ }, [autoScroll, clearInactivityTimer]);
161
+ (0, import_react.useEffect)(() => {
162
+ loopPointRef.current = loopPoint;
163
+ renderPosition(positionRef.current);
164
+ }, [loopPoint, renderPosition]);
165
+ (0, import_react.useEffect)(() => {
166
+ const rafId = requestAnimationFrame(recalculateLoopPoint);
167
+ return () => {
168
+ cancelAnimationFrame(rafId);
169
+ };
170
+ }, [children, recalculateLoopPoint]);
171
+ const pauseAutoScroll = (0, import_react.useCallback)(() => {
172
+ if (!pauseOnInteraction) return;
173
+ autoScrollRef.current.active = false;
174
+ onPauseRef.current?.();
175
+ clearInactivityTimer();
176
+ }, [clearInactivityTimer, pauseOnInteraction]);
177
+ const scheduleAutoScrollResume = (0, import_react.useCallback)(() => {
178
+ if (!pauseOnInteraction || !autoScrollRef.current.desired || isPausedRef.current) return;
179
+ clearInactivityTimer();
180
+ inactivityTimerRef.current = setTimeout(() => {
181
+ inactivityTimerRef.current = null;
182
+ if (!autoScrollRef.current.desired || isPausedRef.current) return;
183
+ autoScrollRef.current.active = true;
184
+ onResumeRef.current?.();
185
+ }, normalizedResumeDelay);
186
+ }, [clearInactivityTimer, normalizedResumeDelay, pauseOnInteraction]);
136
187
  const togglePause = (0, import_react.useCallback)(() => {
137
- setIsPaused((prev) => {
138
- const newPausedState = !prev;
139
- if (newPausedState) {
140
- pauseAutoScroll();
141
- } else {
142
- setAutoScrollEnabled(true);
188
+ const newPaused = !isPausedRef.current;
189
+ isPausedRef.current = newPaused;
190
+ setIsPaused(newPaused);
191
+ if (newPaused) {
192
+ clearInactivityTimer();
193
+ autoScrollRef.current.active = false;
194
+ onPauseRef.current?.();
195
+ } else {
196
+ clearInactivityTimer();
197
+ autoScrollRef.current.active = true;
198
+ if (autoScrollRef.current.desired) {
199
+ onResumeRef.current?.();
143
200
  }
144
- return newPausedState;
145
- });
146
- }, [pauseAutoScroll]);
201
+ }
202
+ }, [clearInactivityTimer]);
147
203
  const handleKeyDown = (0, import_react.useCallback)((e) => {
204
+ if (!keyboard) return;
148
205
  switch (e.key) {
149
206
  case " ":
150
207
  e.preventDefault();
151
208
  togglePause();
152
209
  break;
153
- case "ArrowUp":
210
+ case "ArrowDown":
154
211
  e.preventDefault();
155
- setVelocity((prev) => prev + 15);
212
+ velocityRef.current -= ARROW_KEY_VELOCITY;
156
213
  pauseAutoScroll();
157
214
  scheduleAutoScrollResume();
158
215
  break;
159
- case "ArrowDown":
216
+ case "ArrowUp":
160
217
  e.preventDefault();
161
- setVelocity((prev) => prev - 15);
218
+ velocityRef.current += ARROW_KEY_VELOCITY;
162
219
  pauseAutoScroll();
163
220
  scheduleAutoScrollResume();
164
221
  break;
165
- case "Home":
222
+ case "End":
166
223
  e.preventDefault();
167
- setPosition(0);
168
- setVelocity(0);
224
+ if (loopPointRef.current > 0) {
225
+ commitPosition(-loopPointRef.current);
226
+ }
227
+ velocityRef.current = 0;
169
228
  pauseAutoScroll();
170
229
  scheduleAutoScrollResume();
171
230
  break;
172
- case "End":
231
+ case "Home":
173
232
  e.preventDefault();
174
- if (loopPoint > 0) {
175
- setPosition(-loopPoint);
176
- }
177
- setVelocity(0);
233
+ commitPosition(0);
234
+ velocityRef.current = 0;
178
235
  pauseAutoScroll();
179
236
  scheduleAutoScrollResume();
180
237
  break;
181
238
  default:
182
239
  break;
183
240
  }
184
- }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
185
- const handleResize = (0, import_react.useCallback)(() => {
186
- setContainerHeight(window.innerHeight);
187
- if (containerRef.current) {
188
- const currentContentHeight = containerRef.current.scrollHeight;
189
- setContentHeight(currentContentHeight);
190
- setLoopPoint(currentContentHeight / 3);
191
- }
192
- }, []);
241
+ }, [commitPosition, keyboard, pauseAutoScroll, scheduleAutoScrollResume, togglePause]);
242
+ const handleMouseDown = (0, import_react.useCallback)((e) => {
243
+ if (!draggable) return;
244
+ e.preventDefault();
245
+ containerRef.current?.focus();
246
+ setIsDragging(true);
247
+ isDraggingRef.current = true;
248
+ lastMouseYRef.current = e.clientY;
249
+ velocityRef.current = 0;
250
+ pauseAutoScroll();
251
+ }, [draggable, pauseAutoScroll]);
252
+ const handleMouseMove = (0, import_react.useCallback)((e) => {
253
+ if (!isDraggingRef.current) return;
254
+ e.preventDefault();
255
+ const deltaY = e.clientY - lastMouseYRef.current;
256
+ const nextPosition = wrapPosition(positionRef.current + deltaY);
257
+ commitPosition(nextPosition);
258
+ velocityRef.current = deltaY;
259
+ lastMouseYRef.current = e.clientY;
260
+ }, [commitPosition, wrapPosition]);
261
+ const handleMouseUp = (0, import_react.useCallback)((e) => {
262
+ if (!isDraggingRef.current) return;
263
+ e.preventDefault();
264
+ setIsDragging(false);
265
+ isDraggingRef.current = false;
266
+ scheduleAutoScrollResume();
267
+ }, [scheduleAutoScrollResume]);
268
+ const handleTouchEnd = (0, import_react.useCallback)((_e) => {
269
+ if (!isDraggingRef.current) return;
270
+ setIsDragging(false);
271
+ isDraggingRef.current = false;
272
+ scheduleAutoScrollResume();
273
+ }, [scheduleAutoScrollResume]);
274
+ const handleTouchMove = (0, import_react.useCallback)((e) => {
275
+ if (!isDraggingRef.current || e.touches.length !== 1) return;
276
+ e.preventDefault();
277
+ const touch = e.touches[0];
278
+ const deltaY = touch.clientY - lastTouchYRef.current;
279
+ const nextPosition = wrapPosition(positionRef.current + deltaY);
280
+ commitPosition(nextPosition);
281
+ velocityRef.current = deltaY;
282
+ lastTouchYRef.current = touch.clientY;
283
+ }, [commitPosition, wrapPosition]);
284
+ const handleTouchStart = (0, import_react.useCallback)((e) => {
285
+ if (!draggable || e.touches.length !== 1) return;
286
+ containerRef.current?.focus();
287
+ setIsDragging(true);
288
+ isDraggingRef.current = true;
289
+ lastTouchYRef.current = e.touches[0].clientY;
290
+ velocityRef.current = 0;
291
+ pauseAutoScroll();
292
+ }, [draggable, pauseAutoScroll]);
293
+ const handleWheel = (0, import_react.useCallback)((e) => {
294
+ if (!wheelEnabled) return;
295
+ e.preventDefault();
296
+ velocityRef.current -= e.deltaY * WHEEL_VELOCITY_MULTIPLIER;
297
+ velocityRef.current = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, velocityRef.current));
298
+ pauseAutoScroll();
299
+ scheduleAutoScrollResume();
300
+ }, [pauseAutoScroll, scheduleAutoScrollResume, wheelEnabled]);
193
301
  (0, import_react.useEffect)(() => {
194
302
  const currentContainer = containerRef.current;
195
303
  if (!currentContainer) return;
196
- const boundHandlers = {
197
- mouseDown: handleMouseDown,
198
- mouseMove: handleMouseMove,
199
- mouseUp: handleMouseUp,
200
- touchStart: handleTouchStart,
201
- touchMove: handleTouchMove,
202
- touchEnd: handleTouchEnd,
203
- wheel: handleWheel
304
+ currentContainer.addEventListener("mousedown", handleMouseDown);
305
+ window.addEventListener("mousemove", handleMouseMove);
306
+ window.addEventListener("mouseup", handleMouseUp);
307
+ currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
308
+ window.addEventListener("touchmove", handleTouchMove, { passive: false });
309
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
310
+ currentContainer.addEventListener("wheel", handleWheel, { passive: false });
311
+ return () => {
312
+ currentContainer.removeEventListener("mousedown", handleMouseDown);
313
+ window.removeEventListener("mousemove", handleMouseMove);
314
+ window.removeEventListener("mouseup", handleMouseUp);
315
+ currentContainer.removeEventListener("touchstart", handleTouchStart);
316
+ window.removeEventListener("touchmove", handleTouchMove);
317
+ window.removeEventListener("touchend", handleTouchEnd);
318
+ currentContainer.removeEventListener("wheel", handleWheel);
204
319
  };
205
- currentContainer.addEventListener("mousedown", boundHandlers.mouseDown);
206
- window.addEventListener("mousemove", boundHandlers.mouseMove);
207
- window.addEventListener("mouseup", boundHandlers.mouseUp);
208
- currentContainer.addEventListener("touchstart", boundHandlers.touchStart, { passive: true });
209
- window.addEventListener("touchmove", boundHandlers.touchMove, { passive: false });
210
- window.addEventListener("touchend", boundHandlers.touchEnd, { passive: true });
211
- currentContainer.addEventListener("wheel", boundHandlers.wheel, { passive: false });
320
+ }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchEnd, handleTouchMove, handleTouchStart, handleWheel]);
321
+ const debouncedRecalculate = (0, import_react.useCallback)(() => {
322
+ if (resizeDebounceTimerRef.current) {
323
+ clearTimeout(resizeDebounceTimerRef.current);
324
+ }
325
+ resizeDebounceTimerRef.current = setTimeout(() => {
326
+ resizeDebounceTimerRef.current = null;
327
+ recalculateLoopPoint();
328
+ }, RESIZE_DEBOUNCE_MS);
329
+ }, [recalculateLoopPoint]);
330
+ (0, import_react.useEffect)(() => {
331
+ window.addEventListener("resize", debouncedRecalculate);
212
332
  return () => {
213
- currentContainer.removeEventListener("mousedown", boundHandlers.mouseDown);
214
- window.removeEventListener("mousemove", boundHandlers.mouseMove);
215
- window.removeEventListener("mouseup", boundHandlers.mouseUp);
216
- currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
217
- window.removeEventListener("touchmove", boundHandlers.touchMove);
218
- window.removeEventListener("touchend", boundHandlers.touchEnd);
219
- currentContainer.removeEventListener("wheel", boundHandlers.wheel);
333
+ window.removeEventListener("resize", debouncedRecalculate);
220
334
  };
221
- }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
335
+ }, [debouncedRecalculate]);
222
336
  (0, import_react.useEffect)(() => {
223
- document.addEventListener("keydown", handleKeyDown);
224
- window.addEventListener("resize", handleResize);
337
+ if (!containerRef.current || typeof ResizeObserver === "undefined") return;
338
+ const observer = new ResizeObserver(() => {
339
+ debouncedRecalculate();
340
+ });
341
+ observer.observe(containerRef.current);
225
342
  return () => {
226
- document.removeEventListener("keydown", handleKeyDown);
227
- window.removeEventListener("resize", handleResize);
343
+ observer.disconnect();
228
344
  };
229
- }, [handleKeyDown, handleResize]);
345
+ }, [debouncedRecalculate]);
346
+ (0, import_react.useEffect)(() => {
347
+ return () => {
348
+ if (resizeDebounceTimerRef.current) {
349
+ clearTimeout(resizeDebounceTimerRef.current);
350
+ resizeDebounceTimerRef.current = null;
351
+ }
352
+ };
353
+ }, []);
230
354
  (0, import_react.useEffect)(() => {
231
355
  const handleVisibilityChange = () => {
232
356
  setIsTabActive(!document.hidden);
@@ -239,6 +363,11 @@ function ReactSway({ children }) {
239
363
  document.removeEventListener("visibilitychange", handleVisibilityChange);
240
364
  };
241
365
  }, []);
366
+ (0, import_react.useEffect)(() => {
367
+ return () => {
368
+ clearInactivityTimer();
369
+ };
370
+ }, [clearInactivityTimer]);
242
371
  (0, import_react.useEffect)(() => {
243
372
  if (!isTabActive || isPaused) {
244
373
  if (animationFrameRef.current) {
@@ -247,41 +376,29 @@ function ReactSway({ children }) {
247
376
  }
248
377
  return;
249
378
  }
379
+ lastFrameTimeRef.current = 0;
380
+ const directionMultiplier = direction === "down" ? 1 : -1;
250
381
  const animate = (currentTime) => {
251
- let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
382
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
252
383
  deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
253
384
  lastFrameTimeRef.current = currentTime;
254
- setPosition((prevPosition) => {
255
- let newPosition = prevPosition;
256
- if (autoScrollEnabled && !isDragging && !isPaused) {
257
- newPosition -= SCROLL_SPEED * deltaTime;
258
- }
259
- return newPosition;
260
- });
261
- setVelocity((prevVelocity) => {
262
- let newVelocity = prevVelocity;
263
- if (Math.abs(newVelocity) > 0.1) {
264
- if (!isDragging) {
265
- setPosition((prev) => prev + newVelocity * deltaTime);
266
- }
267
- newVelocity *= Math.pow(FRICTION, deltaTime);
268
- } else {
269
- newVelocity = 0;
270
- }
271
- return newVelocity;
272
- });
273
- setPosition((prevPosition) => {
274
- let newPosition = prevPosition;
275
- if (loopPoint > 0) {
276
- while (newPosition > 0) {
277
- newPosition -= loopPoint;
278
- }
279
- while (newPosition < -loopPoint * 2) {
280
- newPosition += loopPoint;
281
- }
282
- }
283
- return newPosition;
284
- });
385
+ if (prefersReducedMotion) {
386
+ velocityRef.current = 0;
387
+ } else if (Math.abs(velocityRef.current) > 0.1) {
388
+ velocityRef.current *= Math.pow(normalizedFriction, deltaTime);
389
+ } else {
390
+ velocityRef.current = 0;
391
+ }
392
+ let nextPosition = positionRef.current;
393
+ const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
394
+ if (autoScrollRef.current.desired && autoScrollRef.current.active && !isDraggingRef.current) {
395
+ nextPosition += directionMultiplier * effectiveSpeed * deltaTime;
396
+ }
397
+ if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
398
+ nextPosition += velocityRef.current * deltaTime;
399
+ }
400
+ nextPosition = wrapPosition(nextPosition);
401
+ commitPosition(nextPosition);
285
402
  animationFrameRef.current = requestAnimationFrame(animate);
286
403
  };
287
404
  animationFrameRef.current = requestAnimationFrame(animate);
@@ -290,9 +407,9 @@ function ReactSway({ children }) {
290
407
  cancelAnimationFrame(animationFrameRef.current);
291
408
  }
292
409
  };
293
- }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
410
+ }, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
294
411
  (0, import_react.useEffect)(() => {
295
- if (!containerRef.current) return;
412
+ if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
296
413
  const observer = new IntersectionObserver(
297
414
  (entries) => {
298
415
  entries.forEach((entry) => {
@@ -303,8 +420,8 @@ function ReactSway({ children }) {
303
420
  },
304
421
  {
305
422
  root: null,
306
- rootMargin: "100px",
307
- threshold: 0.01
423
+ rootMargin: lazyRootMargin,
424
+ threshold: lazyThreshold
308
425
  }
309
426
  );
310
427
  const items = containerRef.current.querySelectorAll(".content-item");
@@ -313,46 +430,34 @@ function ReactSway({ children }) {
313
430
  items.forEach((item) => observer.unobserve(item));
314
431
  observer.disconnect();
315
432
  };
316
- }, [children, contentHeight]);
317
- (0, import_react.useEffect)(() => {
318
- const originalBodyStyle = {
319
- touchAction: document.body.style.touchAction,
320
- overflow: document.body.style.overflow
321
- };
322
- document.body.style.touchAction = "none";
323
- document.body.style.overflow = "hidden";
324
- return () => {
325
- document.body.style.touchAction = originalBodyStyle.touchAction;
326
- document.body.style.overflow = originalBodyStyle.overflow;
327
- };
328
- }, []);
433
+ }, [children, lazy, lazyRootMargin, lazyThreshold]);
329
434
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
330
435
  "div",
331
436
  {
332
437
  className: "react-sway-container scroller-content",
333
438
  ref: containerRef,
334
439
  style: {
335
- transform: `translate3d(0, ${visualPosition}px, 0)`,
336
- cursor: isDragging ? "grabbing" : "grab",
440
+ cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
441
+ MozUserSelect: "none",
442
+ msUserSelect: "none",
443
+ overflow: "hidden",
444
+ overscrollBehavior: "contain",
445
+ pointerEvents: "auto",
337
446
  position: "absolute",
338
- width: "100%",
339
- willChange: "transform",
340
- WebkitTransform: "translateZ(0)",
341
447
  touchAction: "none",
448
+ transform: "translate3d(0, 0px, 0)",
342
449
  userSelect: "none",
343
450
  WebkitUserSelect: "none",
344
- msUserSelect: "none",
345
- MozUserSelect: "none",
346
- overscrollBehavior: "contain",
347
- // Ensure it's on top and can receive events
348
- pointerEvents: "auto",
451
+ width: "100%",
452
+ willChange: "transform",
349
453
  zIndex: 1
350
454
  },
351
- tabIndex: 0,
455
+ onKeyDown: keyboard ? handleKeyDown : void 0,
456
+ tabIndex: keyboard ? 0 : void 0,
352
457
  children: [
353
458
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "content-group original", children }),
354
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children }),
355
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children })
459
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children }),
460
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children })
356
461
  ]
357
462
  }
358
463
  );