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