react-sway 0.1.1 → 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,205 +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
- if (currentContentHeight > 0) {
59
- setContentHeight(currentContentHeight);
60
- setLoopPoint(calculatedLoopPoint);
61
- }
62
- setContainerHeight(window.innerHeight);
63
- }
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);
64
107
  };
65
- const rafId = requestAnimationFrame(() => {
66
- calculateDimensions();
67
- });
108
+ mediaQuery.addEventListener("change", handleChange);
68
109
  return () => {
69
- cancelAnimationFrame(rafId);
110
+ mediaQuery.removeEventListener("change", handleChange);
70
111
  };
71
- }, [children]);
72
- const pauseAutoScroll = (0, import_react.useCallback)(() => {
73
- setAutoScrollEnabled(false);
74
- if (inactivityTimerRef.current) {
75
- 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;
76
124
  }
125
+ containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
77
126
  }, []);
78
- const scheduleAutoScrollResume = (0, import_react.useCallback)(() => {
79
- if (inactivityTimerRef.current) {
80
- 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;
81
139
  }
82
- inactivityTimerRef.current = setTimeout(() => {
83
- setAutoScrollEnabled(true);
84
- }, INACTIVITY_DELAY);
140
+ while (wrappedPosition < -currentLoopPoint * 2) {
141
+ wrappedPosition += currentLoopPoint;
142
+ }
143
+ return wrappedPosition;
85
144
  }, []);
86
- const handleMouseDown = (0, import_react.useCallback)((e) => {
87
- e.preventDefault();
88
- setIsDragging(true);
89
- lastMouseYRef.current = e.clientY;
90
- setVelocity(0);
91
- pauseAutoScroll();
92
- }, [pauseAutoScroll]);
93
- const handleMouseMove = (0, import_react.useCallback)((e) => {
94
- if (!isDragging) return;
95
- e.preventDefault();
96
- const deltaY = e.clientY - lastMouseYRef.current;
97
- setPosition((prev) => prev + deltaY);
98
- setVelocity(deltaY);
99
- lastMouseYRef.current = e.clientY;
100
- }, [isDragging]);
101
- const handleMouseUp = (0, import_react.useCallback)((e) => {
102
- if (!isDragging) return;
103
- e.preventDefault();
104
- setIsDragging(false);
105
- scheduleAutoScrollResume();
106
- }, [isDragging, scheduleAutoScrollResume]);
107
- const handleTouchStart = (0, import_react.useCallback)((e) => {
108
- if (e.touches.length === 1) {
109
- setIsDragging(true);
110
- lastTouchYRef.current = e.touches[0].clientY;
111
- setVelocity(0);
112
- 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;
113
159
  }
114
- }, [pauseAutoScroll]);
115
- const handleTouchMove = (0, import_react.useCallback)((e) => {
116
- if (!isDragging || e.touches.length !== 1) return;
117
- e.preventDefault();
118
- const touch = e.touches[0];
119
- const deltaY = touch.clientY - lastTouchYRef.current;
120
- setPosition((prev) => prev + deltaY);
121
- setVelocity(deltaY);
122
- lastTouchYRef.current = touch.clientY;
123
- }, [isDragging]);
124
- const handleTouchEnd = (0, import_react.useCallback)((_e) => {
125
- if (!isDragging) return;
126
- setIsDragging(false);
127
- scheduleAutoScrollResume();
128
- }, [isDragging, scheduleAutoScrollResume]);
129
- const handleWheel = (0, import_react.useCallback)((e) => {
130
- e.preventDefault();
131
- setVelocity((prev) => prev - e.deltaY * 0.3);
132
- pauseAutoScroll();
133
- scheduleAutoScrollResume();
134
- }, [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]);
135
187
  const togglePause = (0, import_react.useCallback)(() => {
136
- setIsPaused((prev) => {
137
- const newPausedState = !prev;
138
- if (newPausedState) {
139
- pauseAutoScroll();
140
- } else {
141
- 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?.();
142
200
  }
143
- return newPausedState;
144
- });
145
- }, [pauseAutoScroll]);
201
+ }
202
+ }, [clearInactivityTimer]);
146
203
  const handleKeyDown = (0, import_react.useCallback)((e) => {
204
+ if (!keyboard) return;
147
205
  switch (e.key) {
148
206
  case " ":
149
207
  e.preventDefault();
150
208
  togglePause();
151
209
  break;
152
- case "ArrowUp":
210
+ case "ArrowDown":
153
211
  e.preventDefault();
154
- setVelocity((prev) => prev + 15);
212
+ velocityRef.current -= ARROW_KEY_VELOCITY;
155
213
  pauseAutoScroll();
156
214
  scheduleAutoScrollResume();
157
215
  break;
158
- case "ArrowDown":
216
+ case "ArrowUp":
159
217
  e.preventDefault();
160
- setVelocity((prev) => prev - 15);
218
+ velocityRef.current += ARROW_KEY_VELOCITY;
161
219
  pauseAutoScroll();
162
220
  scheduleAutoScrollResume();
163
221
  break;
164
- case "Home":
222
+ case "End":
165
223
  e.preventDefault();
166
- setPosition(0);
167
- setVelocity(0);
224
+ if (loopPointRef.current > 0) {
225
+ commitPosition(-loopPointRef.current);
226
+ }
227
+ velocityRef.current = 0;
168
228
  pauseAutoScroll();
169
229
  scheduleAutoScrollResume();
170
230
  break;
171
- case "End":
231
+ case "Home":
172
232
  e.preventDefault();
173
- if (loopPoint > 0) {
174
- setPosition(-loopPoint);
175
- }
176
- setVelocity(0);
233
+ commitPosition(0);
234
+ velocityRef.current = 0;
177
235
  pauseAutoScroll();
178
236
  scheduleAutoScrollResume();
179
237
  break;
180
238
  default:
181
239
  break;
182
240
  }
183
- }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
184
- const handleResize = (0, import_react.useCallback)(() => {
185
- setContainerHeight(window.innerHeight);
186
- if (containerRef.current) {
187
- const currentContentHeight = containerRef.current.scrollHeight;
188
- setContentHeight(currentContentHeight);
189
- setLoopPoint(currentContentHeight / 3);
190
- }
191
- }, []);
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]);
192
301
  (0, import_react.useEffect)(() => {
193
302
  const currentContainer = containerRef.current;
194
303
  if (!currentContainer) return;
195
- const boundHandlers = {
196
- mouseDown: handleMouseDown,
197
- mouseMove: handleMouseMove,
198
- mouseUp: handleMouseUp,
199
- touchStart: handleTouchStart,
200
- touchMove: handleTouchMove,
201
- touchEnd: handleTouchEnd,
202
- 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);
203
319
  };
204
- currentContainer.addEventListener("mousedown", boundHandlers.mouseDown);
205
- window.addEventListener("mousemove", boundHandlers.mouseMove);
206
- window.addEventListener("mouseup", boundHandlers.mouseUp);
207
- currentContainer.addEventListener("touchstart", boundHandlers.touchStart, { passive: true });
208
- window.addEventListener("touchmove", boundHandlers.touchMove, { passive: false });
209
- window.addEventListener("touchend", boundHandlers.touchEnd, { passive: true });
210
- 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);
211
332
  return () => {
212
- currentContainer.removeEventListener("mousedown", boundHandlers.mouseDown);
213
- window.removeEventListener("mousemove", boundHandlers.mouseMove);
214
- window.removeEventListener("mouseup", boundHandlers.mouseUp);
215
- currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
216
- window.removeEventListener("touchmove", boundHandlers.touchMove);
217
- window.removeEventListener("touchend", boundHandlers.touchEnd);
218
- currentContainer.removeEventListener("wheel", boundHandlers.wheel);
333
+ window.removeEventListener("resize", debouncedRecalculate);
219
334
  };
220
- }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
335
+ }, [debouncedRecalculate]);
221
336
  (0, import_react.useEffect)(() => {
222
- document.addEventListener("keydown", handleKeyDown);
223
- 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);
224
342
  return () => {
225
- document.removeEventListener("keydown", handleKeyDown);
226
- window.removeEventListener("resize", handleResize);
343
+ observer.disconnect();
227
344
  };
228
- }, [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
+ }, []);
229
354
  (0, import_react.useEffect)(() => {
230
355
  const handleVisibilityChange = () => {
231
356
  setIsTabActive(!document.hidden);
@@ -238,6 +363,11 @@ function ReactSway({ children }) {
238
363
  document.removeEventListener("visibilitychange", handleVisibilityChange);
239
364
  };
240
365
  }, []);
366
+ (0, import_react.useEffect)(() => {
367
+ return () => {
368
+ clearInactivityTimer();
369
+ };
370
+ }, [clearInactivityTimer]);
241
371
  (0, import_react.useEffect)(() => {
242
372
  if (!isTabActive || isPaused) {
243
373
  if (animationFrameRef.current) {
@@ -246,41 +376,29 @@ function ReactSway({ children }) {
246
376
  }
247
377
  return;
248
378
  }
379
+ lastFrameTimeRef.current = 0;
380
+ const directionMultiplier = direction === "down" ? 1 : -1;
249
381
  const animate = (currentTime) => {
250
- let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
382
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
251
383
  deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
252
384
  lastFrameTimeRef.current = currentTime;
253
- setPosition((prevPosition) => {
254
- let newPosition = prevPosition;
255
- if (autoScrollEnabled && !isDragging && !isPaused) {
256
- newPosition -= SCROLL_SPEED * deltaTime;
257
- }
258
- return newPosition;
259
- });
260
- setVelocity((prevVelocity) => {
261
- let newVelocity = prevVelocity;
262
- if (Math.abs(newVelocity) > 0.1) {
263
- if (!isDragging) {
264
- setPosition((prev) => prev + newVelocity * deltaTime);
265
- }
266
- newVelocity *= Math.pow(FRICTION, deltaTime);
267
- } else {
268
- newVelocity = 0;
269
- }
270
- return newVelocity;
271
- });
272
- setPosition((prevPosition) => {
273
- let newPosition = prevPosition;
274
- if (loopPoint > 0) {
275
- while (newPosition > 0) {
276
- newPosition -= loopPoint;
277
- }
278
- while (newPosition < -loopPoint * 2) {
279
- newPosition += loopPoint;
280
- }
281
- }
282
- return newPosition;
283
- });
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);
284
402
  animationFrameRef.current = requestAnimationFrame(animate);
285
403
  };
286
404
  animationFrameRef.current = requestAnimationFrame(animate);
@@ -289,9 +407,9 @@ function ReactSway({ children }) {
289
407
  cancelAnimationFrame(animationFrameRef.current);
290
408
  }
291
409
  };
292
- }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
410
+ }, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
293
411
  (0, import_react.useEffect)(() => {
294
- if (!containerRef.current) return;
412
+ if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
295
413
  const observer = new IntersectionObserver(
296
414
  (entries) => {
297
415
  entries.forEach((entry) => {
@@ -302,8 +420,8 @@ function ReactSway({ children }) {
302
420
  },
303
421
  {
304
422
  root: null,
305
- rootMargin: "100px",
306
- threshold: 0.01
423
+ rootMargin: lazyRootMargin,
424
+ threshold: lazyThreshold
307
425
  }
308
426
  );
309
427
  const items = containerRef.current.querySelectorAll(".content-item");
@@ -312,36 +430,34 @@ function ReactSway({ children }) {
312
430
  items.forEach((item) => observer.unobserve(item));
313
431
  observer.disconnect();
314
432
  };
315
- }, [children, contentHeight]);
433
+ }, [children, lazy, lazyRootMargin, lazyThreshold]);
316
434
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
317
435
  "div",
318
436
  {
319
437
  className: "react-sway-container scroller-content",
320
438
  ref: containerRef,
321
439
  style: {
322
- transform: `translate3d(0, ${visualPosition}px, 0)`,
323
- 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",
324
446
  position: "absolute",
325
- width: "100%",
326
- willChange: "transform",
327
- WebkitTransform: "translateZ(0)",
328
447
  touchAction: "none",
448
+ transform: "translate3d(0, 0px, 0)",
329
449
  userSelect: "none",
330
450
  WebkitUserSelect: "none",
331
- msUserSelect: "none",
332
- MozUserSelect: "none",
333
- overscrollBehavior: "contain",
334
- overflow: "hidden",
335
- // Prevent component's content from overflowing and causing page scroll
336
- // Ensure it's on top and can receive events
337
- pointerEvents: "auto",
451
+ width: "100%",
452
+ willChange: "transform",
338
453
  zIndex: 1
339
454
  },
340
- tabIndex: 0,
455
+ onKeyDown: keyboard ? handleKeyDown : void 0,
456
+ tabIndex: keyboard ? 0 : void 0,
341
457
  children: [
342
458
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "content-group original", children }),
343
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children }),
344
- /* @__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 })
345
461
  ]
346
462
  }
347
463
  );