react-sway 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,205 +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
- if (currentContentHeight > 0) {
33
- setContentHeight(currentContentHeight);
34
- setLoopPoint(calculatedLoopPoint);
35
- }
36
- setContainerHeight(window.innerHeight);
37
- }
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);
38
81
  };
39
- const rafId = requestAnimationFrame(() => {
40
- calculateDimensions();
41
- });
82
+ mediaQuery.addEventListener("change", handleChange);
42
83
  return () => {
43
- cancelAnimationFrame(rafId);
84
+ mediaQuery.removeEventListener("change", handleChange);
44
85
  };
45
- }, [children]);
46
- const pauseAutoScroll = useCallback(() => {
47
- setAutoScrollEnabled(false);
48
- if (inactivityTimerRef.current) {
49
- 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;
50
98
  }
99
+ containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
51
100
  }, []);
52
- const scheduleAutoScrollResume = useCallback(() => {
53
- if (inactivityTimerRef.current) {
54
- 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;
55
113
  }
56
- inactivityTimerRef.current = setTimeout(() => {
57
- setAutoScrollEnabled(true);
58
- }, INACTIVITY_DELAY);
114
+ while (wrappedPosition < -currentLoopPoint * 2) {
115
+ wrappedPosition += currentLoopPoint;
116
+ }
117
+ return wrappedPosition;
59
118
  }, []);
60
- const handleMouseDown = useCallback((e) => {
61
- e.preventDefault();
62
- setIsDragging(true);
63
- lastMouseYRef.current = e.clientY;
64
- setVelocity(0);
65
- pauseAutoScroll();
66
- }, [pauseAutoScroll]);
67
- const handleMouseMove = useCallback((e) => {
68
- if (!isDragging) return;
69
- e.preventDefault();
70
- const deltaY = e.clientY - lastMouseYRef.current;
71
- setPosition((prev) => prev + deltaY);
72
- setVelocity(deltaY);
73
- lastMouseYRef.current = e.clientY;
74
- }, [isDragging]);
75
- const handleMouseUp = useCallback((e) => {
76
- if (!isDragging) return;
77
- e.preventDefault();
78
- setIsDragging(false);
79
- scheduleAutoScrollResume();
80
- }, [isDragging, scheduleAutoScrollResume]);
81
- const handleTouchStart = useCallback((e) => {
82
- if (e.touches.length === 1) {
83
- setIsDragging(true);
84
- lastTouchYRef.current = e.touches[0].clientY;
85
- setVelocity(0);
86
- 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;
87
133
  }
88
- }, [pauseAutoScroll]);
89
- const handleTouchMove = useCallback((e) => {
90
- if (!isDragging || e.touches.length !== 1) return;
91
- e.preventDefault();
92
- const touch = e.touches[0];
93
- const deltaY = touch.clientY - lastTouchYRef.current;
94
- setPosition((prev) => prev + deltaY);
95
- setVelocity(deltaY);
96
- lastTouchYRef.current = touch.clientY;
97
- }, [isDragging]);
98
- const handleTouchEnd = useCallback((_e) => {
99
- if (!isDragging) return;
100
- setIsDragging(false);
101
- scheduleAutoScrollResume();
102
- }, [isDragging, scheduleAutoScrollResume]);
103
- const handleWheel = useCallback((e) => {
104
- e.preventDefault();
105
- setVelocity((prev) => prev - e.deltaY * 0.3);
106
- pauseAutoScroll();
107
- scheduleAutoScrollResume();
108
- }, [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]);
109
161
  const togglePause = useCallback(() => {
110
- setIsPaused((prev) => {
111
- const newPausedState = !prev;
112
- if (newPausedState) {
113
- pauseAutoScroll();
114
- } else {
115
- 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?.();
116
174
  }
117
- return newPausedState;
118
- });
119
- }, [pauseAutoScroll]);
175
+ }
176
+ }, [clearInactivityTimer]);
120
177
  const handleKeyDown = useCallback((e) => {
178
+ if (!keyboard) return;
121
179
  switch (e.key) {
122
180
  case " ":
123
181
  e.preventDefault();
124
182
  togglePause();
125
183
  break;
126
- case "ArrowUp":
184
+ case "ArrowDown":
127
185
  e.preventDefault();
128
- setVelocity((prev) => prev + 15);
186
+ velocityRef.current -= ARROW_KEY_VELOCITY;
129
187
  pauseAutoScroll();
130
188
  scheduleAutoScrollResume();
131
189
  break;
132
- case "ArrowDown":
190
+ case "ArrowUp":
133
191
  e.preventDefault();
134
- setVelocity((prev) => prev - 15);
192
+ velocityRef.current += ARROW_KEY_VELOCITY;
135
193
  pauseAutoScroll();
136
194
  scheduleAutoScrollResume();
137
195
  break;
138
- case "Home":
196
+ case "End":
139
197
  e.preventDefault();
140
- setPosition(0);
141
- setVelocity(0);
198
+ if (loopPointRef.current > 0) {
199
+ commitPosition(-loopPointRef.current);
200
+ }
201
+ velocityRef.current = 0;
142
202
  pauseAutoScroll();
143
203
  scheduleAutoScrollResume();
144
204
  break;
145
- case "End":
205
+ case "Home":
146
206
  e.preventDefault();
147
- if (loopPoint > 0) {
148
- setPosition(-loopPoint);
149
- }
150
- setVelocity(0);
207
+ commitPosition(0);
208
+ velocityRef.current = 0;
151
209
  pauseAutoScroll();
152
210
  scheduleAutoScrollResume();
153
211
  break;
154
212
  default:
155
213
  break;
156
214
  }
157
- }, [togglePause, pauseAutoScroll, scheduleAutoScrollResume, loopPoint]);
158
- const handleResize = useCallback(() => {
159
- setContainerHeight(window.innerHeight);
160
- if (containerRef.current) {
161
- const currentContentHeight = containerRef.current.scrollHeight;
162
- setContentHeight(currentContentHeight);
163
- setLoopPoint(currentContentHeight / 3);
164
- }
165
- }, []);
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]);
166
275
  useEffect(() => {
167
276
  const currentContainer = containerRef.current;
168
277
  if (!currentContainer) return;
169
- const boundHandlers = {
170
- mouseDown: handleMouseDown,
171
- mouseMove: handleMouseMove,
172
- mouseUp: handleMouseUp,
173
- touchStart: handleTouchStart,
174
- touchMove: handleTouchMove,
175
- touchEnd: handleTouchEnd,
176
- 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);
177
293
  };
178
- currentContainer.addEventListener("mousedown", boundHandlers.mouseDown);
179
- window.addEventListener("mousemove", boundHandlers.mouseMove);
180
- window.addEventListener("mouseup", boundHandlers.mouseUp);
181
- currentContainer.addEventListener("touchstart", boundHandlers.touchStart, { passive: true });
182
- window.addEventListener("touchmove", boundHandlers.touchMove, { passive: false });
183
- window.addEventListener("touchend", boundHandlers.touchEnd, { passive: true });
184
- 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);
185
306
  return () => {
186
- currentContainer.removeEventListener("mousedown", boundHandlers.mouseDown);
187
- window.removeEventListener("mousemove", boundHandlers.mouseMove);
188
- window.removeEventListener("mouseup", boundHandlers.mouseUp);
189
- currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
190
- window.removeEventListener("touchmove", boundHandlers.touchMove);
191
- window.removeEventListener("touchend", boundHandlers.touchEnd);
192
- currentContainer.removeEventListener("wheel", boundHandlers.wheel);
307
+ window.removeEventListener("resize", debouncedRecalculate);
193
308
  };
194
- }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd, handleWheel]);
309
+ }, [debouncedRecalculate]);
195
310
  useEffect(() => {
196
- document.addEventListener("keydown", handleKeyDown);
197
- 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);
198
316
  return () => {
199
- document.removeEventListener("keydown", handleKeyDown);
200
- window.removeEventListener("resize", handleResize);
317
+ observer.disconnect();
201
318
  };
202
- }, [handleKeyDown, handleResize]);
319
+ }, [debouncedRecalculate]);
320
+ useEffect(() => {
321
+ return () => {
322
+ if (resizeDebounceTimerRef.current) {
323
+ clearTimeout(resizeDebounceTimerRef.current);
324
+ resizeDebounceTimerRef.current = null;
325
+ }
326
+ };
327
+ }, []);
203
328
  useEffect(() => {
204
329
  const handleVisibilityChange = () => {
205
330
  setIsTabActive(!document.hidden);
@@ -212,6 +337,11 @@ function ReactSway({ children }) {
212
337
  document.removeEventListener("visibilitychange", handleVisibilityChange);
213
338
  };
214
339
  }, []);
340
+ useEffect(() => {
341
+ return () => {
342
+ clearInactivityTimer();
343
+ };
344
+ }, [clearInactivityTimer]);
215
345
  useEffect(() => {
216
346
  if (!isTabActive || isPaused) {
217
347
  if (animationFrameRef.current) {
@@ -220,41 +350,29 @@ function ReactSway({ children }) {
220
350
  }
221
351
  return;
222
352
  }
353
+ lastFrameTimeRef.current = 0;
354
+ const directionMultiplier = direction === "down" ? 1 : -1;
223
355
  const animate = (currentTime) => {
224
- let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / 16.667 : 1;
356
+ let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
225
357
  deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
226
358
  lastFrameTimeRef.current = currentTime;
227
- setPosition((prevPosition) => {
228
- let newPosition = prevPosition;
229
- if (autoScrollEnabled && !isDragging && !isPaused) {
230
- newPosition -= SCROLL_SPEED * deltaTime;
231
- }
232
- return newPosition;
233
- });
234
- setVelocity((prevVelocity) => {
235
- let newVelocity = prevVelocity;
236
- if (Math.abs(newVelocity) > 0.1) {
237
- if (!isDragging) {
238
- setPosition((prev) => prev + newVelocity * deltaTime);
239
- }
240
- newVelocity *= Math.pow(FRICTION, deltaTime);
241
- } else {
242
- newVelocity = 0;
243
- }
244
- return newVelocity;
245
- });
246
- setPosition((prevPosition) => {
247
- let newPosition = prevPosition;
248
- if (loopPoint > 0) {
249
- while (newPosition > 0) {
250
- newPosition -= loopPoint;
251
- }
252
- while (newPosition < -loopPoint * 2) {
253
- newPosition += loopPoint;
254
- }
255
- }
256
- return newPosition;
257
- });
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);
258
376
  animationFrameRef.current = requestAnimationFrame(animate);
259
377
  };
260
378
  animationFrameRef.current = requestAnimationFrame(animate);
@@ -263,9 +381,9 @@ function ReactSway({ children }) {
263
381
  cancelAnimationFrame(animationFrameRef.current);
264
382
  }
265
383
  };
266
- }, [isTabActive, autoScrollEnabled, isDragging, isPaused, loopPoint]);
384
+ }, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
267
385
  useEffect(() => {
268
- if (!containerRef.current) return;
386
+ if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
269
387
  const observer = new IntersectionObserver(
270
388
  (entries) => {
271
389
  entries.forEach((entry) => {
@@ -276,8 +394,8 @@ function ReactSway({ children }) {
276
394
  },
277
395
  {
278
396
  root: null,
279
- rootMargin: "100px",
280
- threshold: 0.01
397
+ rootMargin: lazyRootMargin,
398
+ threshold: lazyThreshold
281
399
  }
282
400
  );
283
401
  const items = containerRef.current.querySelectorAll(".content-item");
@@ -286,36 +404,34 @@ function ReactSway({ children }) {
286
404
  items.forEach((item) => observer.unobserve(item));
287
405
  observer.disconnect();
288
406
  };
289
- }, [children, contentHeight]);
407
+ }, [children, lazy, lazyRootMargin, lazyThreshold]);
290
408
  return /* @__PURE__ */ jsxs(
291
409
  "div",
292
410
  {
293
411
  className: "react-sway-container scroller-content",
294
412
  ref: containerRef,
295
413
  style: {
296
- transform: `translate3d(0, ${visualPosition}px, 0)`,
297
- 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",
298
420
  position: "absolute",
299
- width: "100%",
300
- willChange: "transform",
301
- WebkitTransform: "translateZ(0)",
302
421
  touchAction: "none",
422
+ transform: "translate3d(0, 0px, 0)",
303
423
  userSelect: "none",
304
424
  WebkitUserSelect: "none",
305
- msUserSelect: "none",
306
- MozUserSelect: "none",
307
- overscrollBehavior: "contain",
308
- overflow: "hidden",
309
- // Prevent component's content from overflowing and causing page scroll
310
- // Ensure it's on top and can receive events
311
- pointerEvents: "auto",
425
+ width: "100%",
426
+ willChange: "transform",
312
427
  zIndex: 1
313
428
  },
314
- tabIndex: 0,
429
+ onKeyDown: keyboard ? handleKeyDown : void 0,
430
+ tabIndex: keyboard ? 0 : void 0,
315
431
  children: [
316
432
  /* @__PURE__ */ jsx("div", { className: "content-group original", children }),
317
- /* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "aria-hidden": "true", "data-duplicate": "true", role: "presentation", children }),
318
- /* @__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 })
319
435
  ]
320
436
  }
321
437
  );