react-sway 0.3.0 → 0.4.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
@@ -1,36 +1,86 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
3
  let react_jsx_runtime = require("react/jsx-runtime");
4
- //#region src/ReactSway.tsx
4
+ //#region src/edge-hover-velocity.ts
5
5
  /**
6
- * Core ReactSway component implementing axis-aware infinite scrolling interactions.
6
+ * Defines the bounded spatial mapping from edge-hover depth to Sway velocity.
7
7
  */
8
- /** Velocity applied per arrow key press (pixels). */
9
- var ARROW_KEY_VELOCITY = 15;
10
8
  /** Default edge-hover activation thickness in pixels. */
11
9
  var DEFAULT_EDGE_HOVER_SIZE = 96;
12
- /** Default friction coefficient applied to velocity each frame. */
13
- var DEFAULT_FRICTION = .95;
14
- /** Default IntersectionObserver rootMargin for lazy visibility detection. */
15
- var DEFAULT_LAZY_ROOT_MARGIN = "100px";
16
- /** Default IntersectionObserver threshold for lazy visibility detection. */
17
- var DEFAULT_LAZY_THRESHOLD = .01;
18
- /** Default delay in milliseconds before auto-scroll resumes after user interaction. */
19
- var DEFAULT_RESUME_DELAY = 2e3;
20
- /** Default scroll speed in pixels per frame at 60fps. */
21
- var DEFAULT_SPEED = .5;
22
- /** Number of stacked content groups used to build the seamless loop. */
23
- var MIN_LOOP_SEGMENTS = 3;
24
- /** Maximum deltaTime cap to prevent physics instability during frame drops. */
25
- var MAX_DELTA_TIME = 3;
26
- /** Maximum allowed velocity magnitude to prevent runaway scrolling. */
27
- var MAX_VELOCITY = 150;
28
- /** Duration of a single frame at 60fps in milliseconds. */
29
- var MS_PER_FRAME_60FPS = 16.667;
30
- /** Speed multiplier when user prefers reduced motion (25% of normal). */
31
- var REDUCED_MOTION_SPEED_FACTOR = .25;
32
- /** Debounce delay in milliseconds for ResizeObserver callbacks. */
33
- var RESIZE_DEBOUNCE_MS = 150;
10
+ /** Default maximum edge-hover speed relative to the configured base speed. */
11
+ var DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER = 6;
12
+ /** Upper bound for consumer-configured edge-hover speed amplification. */
13
+ var MAX_EDGE_HOVER_SPEED_MULTIPLIER = 10;
14
+ /** Normalized ingress band used to remove the zero-to-baseline velocity jump. */
15
+ var EDGE_HOVER_ENTRY_FEATHER_INTENSITY = .08;
16
+ /** Intensity reached at the physical edge of the visible viewport. */
17
+ var EDGE_HOVER_PHYSICAL_EDGE_INTENSITY = .5;
18
+ /** Historical physical-edge response retained before beyond-edge acceleration. */
19
+ var EDGE_HOVER_PHYSICAL_EDGE_SPEED_MULTIPLIER = 3;
20
+ function clampUnit(value) {
21
+ if (!Number.isFinite(value)) return 0;
22
+ return Math.max(0, Math.min(1, value));
23
+ }
24
+ function smoothUnitStep(value) {
25
+ const unitValue = clampUnit(value);
26
+ return unitValue * unitValue * (3 - 2 * unitValue);
27
+ }
28
+ function normalizeEdgeHoverIntensity(value) {
29
+ if (!Number.isFinite(value)) return 0;
30
+ const clampedValue = Math.max(-1, Math.min(1, value));
31
+ return clampedValue === 0 ? 0 : clampedValue;
32
+ }
33
+ function normalizeEdgeHoverSpeedMultiplier(value) {
34
+ if (!Number.isFinite(value)) return 6;
35
+ return Math.max(1, Math.min(MAX_EDGE_HOVER_SPEED_MULTIPLIER, value));
36
+ }
37
+ /**
38
+ * Maps signed edge depth onto a bounded velocity scale.
39
+ *
40
+ * Intensity carries direction and lies in [-1, 1]. The speed multiplier is
41
+ * the maximum amplification reached one activation-zone width beyond the
42
+ * visible edge.
43
+ */
44
+ function getEdgeHoverVelocityScale(intensity, speedMultiplier = 6) {
45
+ const normalizedIntensity = normalizeEdgeHoverIntensity(intensity);
46
+ if (normalizedIntensity === 0) return 0;
47
+ const direction = Math.sign(normalizedIntensity);
48
+ const depth = Math.abs(normalizedIntensity);
49
+ const maximumScale = normalizeEdgeHoverSpeedMultiplier(speedMultiplier);
50
+ if (depth < EDGE_HOVER_ENTRY_FEATHER_INTENSITY) return direction * smoothUnitStep(depth / EDGE_HOVER_ENTRY_FEATHER_INTENSITY);
51
+ const physicalEdgeScale = Math.min(EDGE_HOVER_PHYSICAL_EDGE_SPEED_MULTIPLIER, maximumScale);
52
+ if (depth <= EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) {
53
+ const physicalEdgeProgress = (depth - EDGE_HOVER_ENTRY_FEATHER_INTENSITY) / (EDGE_HOVER_PHYSICAL_EDGE_INTENSITY - EDGE_HOVER_ENTRY_FEATHER_INTENSITY);
54
+ return direction * (1 + (physicalEdgeScale - 1) * smoothUnitStep(physicalEdgeProgress));
55
+ }
56
+ const beyondEdgeProgress = (depth - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) / (1 - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY);
57
+ return direction * (physicalEdgeScale + (maximumScale - physicalEdgeScale) * smoothUnitStep(beyondEdgeProgress));
58
+ }
59
+ /**
60
+ * Computes signed edge depth using a symmetric linear ramp.
61
+ *
62
+ * The inner edge of either activation zone maps to zero, the physical edge
63
+ * maps to half intensity, and motion keeps accelerating outside the viewport
64
+ * until it reaches the cap one activation-zone width beyond the visible edge.
65
+ */
66
+ function getEdgeHoverIntensity(pointerPosition, visibleInterval, edgeHoverSize) {
67
+ const effectiveEdgeHoverSize = Math.min(Math.max(edgeHoverSize, 0), visibleInterval.size / 2);
68
+ if (effectiveEdgeHoverSize <= 0) return 0;
69
+ const accelerationDistance = effectiveEdgeHoverSize * 2;
70
+ const leadingEdgeEnd = visibleInterval.start + effectiveEdgeHoverSize;
71
+ if (pointerPosition <= leadingEdgeEnd) {
72
+ const leadingIntensity = clampUnit((leadingEdgeEnd - pointerPosition) / accelerationDistance);
73
+ if (leadingIntensity > 0) return leadingIntensity;
74
+ }
75
+ const trailingEdgeStart = visibleInterval.end - effectiveEdgeHoverSize;
76
+ if (pointerPosition >= trailingEdgeStart) {
77
+ const trailingIntensity = clampUnit((pointerPosition - trailingEdgeStart) / accelerationDistance);
78
+ if (trailingIntensity > 0) return -trailingIntensity;
79
+ }
80
+ return 0;
81
+ }
82
+ //#endregion
83
+ //#region src/wheel-input.ts
34
84
  /** Fallback pixel height for wheel events reported in line units. */
35
85
  var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
36
86
  /** WheelEvent deltaMode value for line-based deltas. */
@@ -39,24 +89,15 @@ var WHEEL_MODE_LINE = 1;
39
89
  var WHEEL_MODE_PAGE = 2;
40
90
  /** Minimum pixel impulse for discrete wheel hardware reporting tiny pixel deltas. */
41
91
  var WHEEL_PIXEL_MIN_DELTA_PX = 48;
42
- /** Multiplier applied to wheel deltaY to convert to scroll velocity. */
43
- var WHEEL_VELOCITY_MULTIPLIER = .14;
44
- /** Small immediate wheel movement used to keep input responsive without jumping. */
45
- var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
46
92
  /** Legacy wheelDelta magnitude that usually represents one physical wheel notch. */
47
93
  var WHEEL_LEGACY_NOTCH_DELTA = 120;
48
- /** Minimum touch movement before a gesture is classified by axis. */
49
- var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
50
94
  function getLegacyWheelPixelDeltaY(event) {
51
95
  const { wheelDelta, wheelDeltaY } = event;
52
96
  const legacyWheelDelta = typeof wheelDeltaY === "number" ? wheelDeltaY : wheelDelta;
53
97
  if (typeof legacyWheelDelta !== "number" || !Number.isFinite(legacyWheelDelta)) return 0;
54
98
  return -(legacyWheelDelta / WHEEL_LEGACY_NOTCH_DELTA) * WHEEL_PIXEL_MIN_DELTA_PX;
55
99
  }
56
- /**
57
- * Converts wheel deltas to pixels so mouse wheels, touchpads, and page-wheel
58
- * devices feed the same Sway velocity system.
59
- */
100
+ /** Converts line, page, modern pixel, and legacy wheel deltas to pixels. */
60
101
  function normalizeWheelDelta(event, container, axis) {
61
102
  const rawDelta = axis === "x" ? event.deltaX : event.deltaY;
62
103
  if (event.deltaMode === WHEEL_MODE_LINE) {
@@ -69,6 +110,10 @@ function normalizeWheelDelta(event, container, axis) {
69
110
  if (Math.abs(rawDelta) < WHEEL_PIXEL_MIN_DELTA_PX && Math.abs(legacyWheelPixelDeltaY) >= WHEEL_PIXEL_MIN_DELTA_PX) return legacyWheelPixelDeltaY;
70
111
  return rawDelta;
71
112
  }
113
+ /**
114
+ * Returns Sway's signed pixel delta, or null when native scroll chaining owns
115
+ * the gesture. This partial function is the wheel-ownership boundary.
116
+ */
72
117
  function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
73
118
  const deltaX = normalizeWheelDelta(event, container, "x");
74
119
  const deltaY = normalizeWheelDelta(event, container, "y");
@@ -81,6 +126,41 @@ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
81
126
  if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
82
127
  return deltaY;
83
128
  }
129
+ //#endregion
130
+ //#region src/ReactSway.tsx
131
+ /**
132
+ * Core ReactSway component implementing axis-aware infinite scrolling interactions.
133
+ */
134
+ /** Velocity applied per arrow key press (pixels). */
135
+ var ARROW_KEY_VELOCITY = 15;
136
+ /** Default friction coefficient applied to velocity each frame. */
137
+ var DEFAULT_FRICTION = .95;
138
+ /** Default IntersectionObserver rootMargin for lazy visibility detection. */
139
+ var DEFAULT_LAZY_ROOT_MARGIN = "100px";
140
+ /** Default IntersectionObserver threshold for lazy visibility detection. */
141
+ var DEFAULT_LAZY_THRESHOLD = .01;
142
+ /** Default delay in milliseconds before auto-scroll resumes after user interaction. */
143
+ var DEFAULT_RESUME_DELAY = 2e3;
144
+ /** Default scroll speed in pixels per frame at 60fps. */
145
+ var DEFAULT_SPEED = .5;
146
+ /** Minimum stacked content groups needed to cover a seamless loop phase. */
147
+ var MIN_LOOP_SEGMENTS = 2;
148
+ /** Maximum deltaTime cap to prevent physics instability during frame drops. */
149
+ var MAX_DELTA_TIME = 3;
150
+ /** Edge-hover catch-up cap; larger jumps read as stalls followed by snapping. */
151
+ var MAX_EDGE_HOVER_DELTA_TIME = 1;
152
+ /** Maximum allowed velocity magnitude to prevent runaway scrolling. */
153
+ var MAX_VELOCITY = 150;
154
+ /** Duration of a single frame at 60fps in milliseconds. */
155
+ var MS_PER_FRAME_60FPS = 16.667;
156
+ /** Speed multiplier when user prefers reduced motion (25% of normal). */
157
+ var REDUCED_MOTION_SPEED_FACTOR = .25;
158
+ /** Multiplier applied to wheel deltaY to convert to scroll velocity. */
159
+ var WHEEL_VELOCITY_MULTIPLIER = .14;
160
+ /** Small immediate wheel movement used to keep input responsive without jumping. */
161
+ var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
162
+ /** Minimum touch movement before a gesture is classified by axis. */
163
+ var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
84
164
  function getVisibleEdgeInterval(rect, isHorizontal) {
85
165
  const rectEnd = isHorizontal ? rect.right : rect.bottom;
86
166
  const rectStart = isHorizontal ? rect.left : rect.top;
@@ -114,10 +194,12 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
114
194
  * </ReactSway>
115
195
  * ```
116
196
  */
117
- function ReactSway({ axis, autoScroll = true, children, direction = "up", draggable = true, edgeHoverScroll = false, edgeHoverSize = DEFAULT_EDGE_HOVER_SIZE, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }) {
197
+ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollable content", axis, autoScroll = true, children, className, direction = "up", draggable = true, edgeHoverInputMode = "hybrid", edgeHoverScroll = false, edgeHoverSize = 96, edgeHoverSpeedMultiplier = 6, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }, ref) {
118
198
  const isHorizontal = (0, react.useMemo)(() => axis ?? (direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]) === "horizontal";
119
- const normalizedEdgeHoverSize = (0, react.useMemo)(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : DEFAULT_EDGE_HOVER_SIZE, [edgeHoverSize]);
199
+ const normalizedEdgeHoverSize = (0, react.useMemo)(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : 96, [edgeHoverSize]);
200
+ const normalizedEdgeHoverSpeedMultiplier = (0, react.useMemo)(() => normalizeEdgeHoverSpeedMultiplier(edgeHoverSpeedMultiplier), [edgeHoverSpeedMultiplier]);
120
201
  const normalizedFriction = (0, react.useMemo)(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
202
+ const normalizedLazyThreshold = (0, react.useMemo)(() => Number.isFinite(lazyThreshold) ? Math.min(Math.max(lazyThreshold, 0), 1) : DEFAULT_LAZY_THRESHOLD, [lazyThreshold]);
121
203
  const normalizedResumeDelay = (0, react.useMemo)(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
122
204
  const normalizedSpeed = (0, react.useMemo)(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
123
205
  const touchAction = (0, react.useMemo)(() => {
@@ -125,16 +207,15 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
125
207
  return isHorizontal ? "pan-y" : "pan-x";
126
208
  }, [draggable, isHorizontal]);
127
209
  const overscrollBehavior = wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
128
- const [isDragging, setIsDragging] = (0, react.useState)(false);
129
210
  const [isPaused, setIsPaused] = (0, react.useState)(false);
130
- const [isTabActive, setIsTabActive] = (0, react.useState)(true);
131
- const [loopPoint, setLoopPoint] = (0, react.useState)(0);
211
+ const [isTabActive, setIsTabActive] = (0, react.useState)(() => typeof document === "undefined" || !document.hidden);
132
212
  const [loopSegmentCount, setLoopSegmentCount] = (0, react.useState)(MIN_LOOP_SEGMENTS);
133
213
  const [prefersReducedMotion, setPrefersReducedMotion] = (0, react.useState)(() => {
134
214
  if (typeof window === "undefined") return false;
135
215
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
136
216
  });
137
217
  const animationFrameRef = (0, react.useRef)(null);
218
+ const animationLoopRef = (0, react.useRef)(null);
138
219
  const autoScrollRef = (0, react.useRef)({
139
220
  active: autoScroll,
140
221
  desired: autoScroll
@@ -149,13 +230,19 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
149
230
  const lastTouchXRef = (0, react.useRef)(0);
150
231
  const lastTouchYRef = (0, react.useRef)(0);
151
232
  const loopPointRef = (0, react.useRef)(0);
152
- const edgeHoverDirectionRef = (0, react.useRef)(0);
233
+ const edgeHoverIntervalCacheFrameRef = (0, react.useRef)(null);
234
+ const edgeHoverIntervalCacheRef = (0, react.useRef)(null);
235
+ const edgeHoverIntensityRef = (0, react.useRef)(0);
236
+ const externalEdgeHoverLeaseRef = (0, react.useRef)(false);
153
237
  const onPauseRef = (0, react.useRef)(onPause);
154
238
  const onResumeRef = (0, react.useRef)(onResume);
155
239
  const onScrollRef = (0, react.useRef)(onScroll);
156
240
  const originalGroupRef = (0, react.useRef)(null);
157
241
  const positionRef = (0, react.useRef)(0);
158
- const resizeDebounceTimerRef = (0, react.useRef)(null);
242
+ const loopMeasurementFrameRef = (0, react.useRef)(null);
243
+ const lastLoopMeasurementRef = (0, react.useRef)(null);
244
+ const mouseDragListenerCleanupRef = (0, react.useRef)(null);
245
+ const touchDragListenerCleanupRef = (0, react.useRef)(null);
159
246
  const touchInteractionStateRef = (0, react.useRef)("idle");
160
247
  const touchStartXRef = (0, react.useRef)(0);
161
248
  const touchStartYRef = (0, react.useRef)(0);
@@ -185,6 +272,32 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
185
272
  clearTimeout(inactivityTimerRef.current);
186
273
  inactivityTimerRef.current = null;
187
274
  }, []);
275
+ const startAnimationLoop = (0, react.useCallback)(() => {
276
+ if (animationFrameRef.current !== null || !animationLoopRef.current) return;
277
+ animationFrameRef.current = requestAnimationFrame(animationLoopRef.current);
278
+ }, []);
279
+ const clearEdgeHoverIntervalCache = (0, react.useCallback)(() => {
280
+ edgeHoverIntervalCacheRef.current = null;
281
+ if (edgeHoverIntervalCacheFrameRef.current !== null) {
282
+ cancelAnimationFrame(edgeHoverIntervalCacheFrameRef.current);
283
+ edgeHoverIntervalCacheFrameRef.current = null;
284
+ }
285
+ }, []);
286
+ const readVisibleEdgeInterval = (0, react.useCallback)((viewport) => {
287
+ const cachedInterval = edgeHoverIntervalCacheRef.current;
288
+ if (cachedInterval && cachedInterval.viewport === viewport && cachedInterval.isHorizontal === isHorizontal) return cachedInterval.interval;
289
+ const interval = getVisibleEdgeInterval(viewport.getBoundingClientRect(), isHorizontal);
290
+ edgeHoverIntervalCacheRef.current = {
291
+ interval,
292
+ isHorizontal,
293
+ viewport
294
+ };
295
+ if (edgeHoverIntervalCacheFrameRef.current === null) edgeHoverIntervalCacheFrameRef.current = requestAnimationFrame(() => {
296
+ edgeHoverIntervalCacheFrameRef.current = null;
297
+ edgeHoverIntervalCacheRef.current = null;
298
+ });
299
+ return interval;
300
+ }, [isHorizontal]);
188
301
  const renderPosition = (0, react.useCallback)((rawPosition) => {
189
302
  if (!containerRef.current) return;
190
303
  const currentLoopPoint = loopPointRef.current;
@@ -201,22 +314,33 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
201
314
  const wrapPosition = (0, react.useCallback)((rawPosition) => {
202
315
  const currentLoopPoint = loopPointRef.current;
203
316
  if (currentLoopPoint <= 0) return rawPosition;
204
- let wrappedPosition = rawPosition;
205
- while (wrappedPosition > 0) wrappedPosition -= currentLoopPoint;
206
- while (wrappedPosition < -currentLoopPoint * 2) wrappedPosition += currentLoopPoint;
317
+ if (rawPosition <= 0 && rawPosition >= -currentLoopPoint * 2) return rawPosition;
318
+ let wrappedPosition = rawPosition % currentLoopPoint;
319
+ if (wrappedPosition > 0) wrappedPosition -= currentLoopPoint;
207
320
  return wrappedPosition;
208
321
  }, []);
322
+ const setDraggingState = (0, react.useCallback)((nextIsDragging) => {
323
+ isDraggingRef.current = nextIsDragging;
324
+ if (containerRef.current && draggable) containerRef.current.style.cursor = nextIsDragging ? "grabbing" : "grab";
325
+ }, [draggable]);
209
326
  const recalculateLoopPoint = (0, react.useCallback)(() => {
210
327
  if (!containerRef.current || !originalGroupRef.current) return;
211
328
  const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
212
329
  if (originalContentSize <= 0) return;
213
330
  const viewport = containerRef.current.parentElement ?? containerRef.current;
214
331
  const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
215
- const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 2);
332
+ const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
216
333
  const nextLoopPoint = originalContentSize;
334
+ const previousMeasurement = lastLoopMeasurementRef.current;
335
+ const nextMeasurement = {
336
+ contentSize: originalContentSize,
337
+ isHorizontal,
338
+ viewportSize
339
+ };
340
+ if (previousMeasurement?.contentSize === nextMeasurement.contentSize && previousMeasurement.isHorizontal === nextMeasurement.isHorizontal && previousMeasurement.viewportSize === nextMeasurement.viewportSize) return;
341
+ lastLoopMeasurementRef.current = nextMeasurement;
217
342
  setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
218
343
  loopPointRef.current = nextLoopPoint;
219
- setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < .5 ? previousLoopPoint : nextLoopPoint);
220
344
  renderPosition(positionRef.current);
221
345
  }, [isHorizontal, renderPosition]);
222
346
  (0, react.useEffect)(() => {
@@ -224,20 +348,30 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
224
348
  if (!autoScroll) {
225
349
  clearInactivityTimer();
226
350
  autoScrollRef.current.active = true;
227
- }
228
- }, [autoScroll, clearInactivityTimer]);
229
- (0, react.useEffect)(() => {
230
- loopPointRef.current = loopPoint;
231
- renderPosition(positionRef.current);
232
- }, [loopPoint, renderPosition]);
351
+ } else startAnimationLoop();
352
+ }, [
353
+ autoScroll,
354
+ clearInactivityTimer,
355
+ startAnimationLoop
356
+ ]);
357
+ const scheduleLoopPointRecalculation = (0, react.useCallback)(() => {
358
+ clearEdgeHoverIntervalCache();
359
+ if (loopMeasurementFrameRef.current !== null) return;
360
+ loopMeasurementFrameRef.current = requestAnimationFrame(() => {
361
+ loopMeasurementFrameRef.current = null;
362
+ recalculateLoopPoint();
363
+ });
364
+ }, [clearEdgeHoverIntervalCache, recalculateLoopPoint]);
233
365
  (0, react.useEffect)(() => {
234
- const rafId = requestAnimationFrame(recalculateLoopPoint);
235
- return () => {
236
- cancelAnimationFrame(rafId);
237
- };
238
- }, [children, recalculateLoopPoint]);
366
+ lastLoopMeasurementRef.current = null;
367
+ scheduleLoopPointRecalculation();
368
+ }, [
369
+ children,
370
+ isHorizontal,
371
+ scheduleLoopPointRecalculation
372
+ ]);
239
373
  const pauseAutoScroll = (0, react.useCallback)(() => {
240
- if (!pauseOnInteraction) return;
374
+ if (!pauseOnInteraction || !autoScrollRef.current.desired || !autoScrollRef.current.active) return;
241
375
  autoScrollRef.current.active = false;
242
376
  onPauseRef.current?.();
243
377
  clearInactivityTimer();
@@ -250,11 +384,13 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
250
384
  if (!autoScrollRef.current.desired || isPausedRef.current) return;
251
385
  autoScrollRef.current.active = true;
252
386
  onResumeRef.current?.();
387
+ startAnimationLoop();
253
388
  }, normalizedResumeDelay);
254
389
  }, [
255
390
  clearInactivityTimer,
256
391
  normalizedResumeDelay,
257
- pauseOnInteraction
392
+ pauseOnInteraction,
393
+ startAnimationLoop
258
394
  ]);
259
395
  const togglePause = (0, react.useCallback)(() => {
260
396
  const newPaused = !isPausedRef.current;
@@ -268,8 +404,22 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
268
404
  clearInactivityTimer();
269
405
  autoScrollRef.current.active = true;
270
406
  if (autoScrollRef.current.desired) onResumeRef.current?.();
407
+ startAnimationLoop();
271
408
  }
272
- }, [clearInactivityTimer]);
409
+ }, [clearInactivityTimer, startAnimationLoop]);
410
+ const setEdgeHoverIntensity = (0, react.useCallback)((nextIntensityValue) => {
411
+ const nextIntensity = normalizeEdgeHoverIntensity(nextIntensityValue);
412
+ edgeHoverIntensityRef.current = nextIntensity;
413
+ if (nextIntensity !== 0) startAnimationLoop();
414
+ }, [startAnimationLoop]);
415
+ const detachMouseDragListeners = (0, react.useCallback)(() => {
416
+ mouseDragListenerCleanupRef.current?.();
417
+ mouseDragListenerCleanupRef.current = null;
418
+ }, []);
419
+ const detachTouchDragListeners = (0, react.useCallback)(() => {
420
+ touchDragListenerCleanupRef.current?.();
421
+ touchDragListenerCleanupRef.current = null;
422
+ }, []);
273
423
  const handleKeyDown = (0, react.useCallback)((e) => {
274
424
  if (!keyboard) return;
275
425
  switch (e.key) {
@@ -321,25 +471,16 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
321
471
  break;
322
472
  default: break;
323
473
  }
474
+ startAnimationLoop();
324
475
  }, [
325
476
  commitPosition,
326
477
  isHorizontal,
327
478
  keyboard,
328
479
  pauseAutoScroll,
329
480
  scheduleAutoScrollResume,
481
+ startAnimationLoop,
330
482
  togglePause
331
483
  ]);
332
- const handleMouseDown = (0, react.useCallback)((e) => {
333
- if (!draggable) return;
334
- e.preventDefault();
335
- containerRef.current?.focus();
336
- setIsDragging(true);
337
- isDraggingRef.current = true;
338
- lastMouseXRef.current = e.clientX;
339
- lastMouseYRef.current = e.clientY;
340
- velocityRef.current = 0;
341
- pauseAutoScroll();
342
- }, [draggable, pauseAutoScroll]);
343
484
  const handleMouseMove = (0, react.useCallback)((e) => {
344
485
  if (!isDraggingRef.current) return;
345
486
  e.preventDefault();
@@ -357,19 +498,77 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
357
498
  const handleMouseUp = (0, react.useCallback)((e) => {
358
499
  if (!isDraggingRef.current) return;
359
500
  e.preventDefault();
360
- setIsDragging(false);
361
- isDraggingRef.current = false;
501
+ setDraggingState(false);
502
+ detachMouseDragListeners();
503
+ scheduleAutoScrollResume();
504
+ startAnimationLoop();
505
+ }, [
506
+ detachMouseDragListeners,
507
+ scheduleAutoScrollResume,
508
+ setDraggingState,
509
+ startAnimationLoop
510
+ ]);
511
+ const handleMouseCancel = (0, react.useCallback)(() => {
512
+ if (!isDraggingRef.current) return;
513
+ setDraggingState(false);
514
+ detachMouseDragListeners();
362
515
  scheduleAutoScrollResume();
363
- }, [scheduleAutoScrollResume]);
516
+ startAnimationLoop();
517
+ }, [
518
+ detachMouseDragListeners,
519
+ scheduleAutoScrollResume,
520
+ setDraggingState,
521
+ startAnimationLoop
522
+ ]);
523
+ const attachMouseDragListeners = (0, react.useCallback)(() => {
524
+ detachMouseDragListeners();
525
+ window.addEventListener("mousemove", handleMouseMove);
526
+ window.addEventListener("mouseup", handleMouseUp);
527
+ window.addEventListener("blur", handleMouseCancel);
528
+ mouseDragListenerCleanupRef.current = () => {
529
+ window.removeEventListener("mousemove", handleMouseMove);
530
+ window.removeEventListener("mouseup", handleMouseUp);
531
+ window.removeEventListener("blur", handleMouseCancel);
532
+ };
533
+ }, [
534
+ detachMouseDragListeners,
535
+ handleMouseCancel,
536
+ handleMouseMove,
537
+ handleMouseUp
538
+ ]);
539
+ const handleMouseDown = (0, react.useCallback)((e) => {
540
+ if (!draggable) return;
541
+ e.preventDefault();
542
+ containerRef.current?.focus();
543
+ setDraggingState(true);
544
+ lastMouseXRef.current = e.clientX;
545
+ lastMouseYRef.current = e.clientY;
546
+ velocityRef.current = 0;
547
+ pauseAutoScroll();
548
+ attachMouseDragListeners();
549
+ startAnimationLoop();
550
+ }, [
551
+ attachMouseDragListeners,
552
+ draggable,
553
+ pauseAutoScroll,
554
+ setDraggingState,
555
+ startAnimationLoop
556
+ ]);
364
557
  const handleTouchEnd = (0, react.useCallback)((_e) => {
365
558
  const wasActive = touchInteractionStateRef.current === "active";
366
559
  touchInteractionStateRef.current = "idle";
367
- if (isDraggingRef.current) {
368
- setIsDragging(false);
369
- isDraggingRef.current = false;
560
+ detachTouchDragListeners();
561
+ if (isDraggingRef.current) setDraggingState(false);
562
+ if (wasActive) {
563
+ scheduleAutoScrollResume();
564
+ startAnimationLoop();
370
565
  }
371
- if (wasActive) scheduleAutoScrollResume();
372
- }, [scheduleAutoScrollResume]);
566
+ }, [
567
+ detachTouchDragListeners,
568
+ scheduleAutoScrollResume,
569
+ setDraggingState,
570
+ startAnimationLoop
571
+ ]);
373
572
  const handleTouchMove = (0, react.useCallback)((e) => {
374
573
  if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
375
574
  if (e.touches.length !== 1) return;
@@ -385,8 +584,7 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
385
584
  return;
386
585
  }
387
586
  touchInteractionStateRef.current = "active";
388
- setIsDragging(true);
389
- isDraggingRef.current = true;
587
+ setDraggingState(true);
390
588
  pauseAutoScroll();
391
589
  }
392
590
  e.preventDefault();
@@ -400,8 +598,24 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
400
598
  commitPosition,
401
599
  isHorizontal,
402
600
  pauseAutoScroll,
601
+ setDraggingState,
403
602
  wrapPosition
404
603
  ]);
604
+ const attachTouchDragListeners = (0, react.useCallback)(() => {
605
+ detachTouchDragListeners();
606
+ window.addEventListener("touchmove", handleTouchMove, { passive: false });
607
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
608
+ window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
609
+ touchDragListenerCleanupRef.current = () => {
610
+ window.removeEventListener("touchmove", handleTouchMove);
611
+ window.removeEventListener("touchend", handleTouchEnd);
612
+ window.removeEventListener("touchcancel", handleTouchEnd);
613
+ };
614
+ }, [
615
+ detachTouchDragListeners,
616
+ handleTouchEnd,
617
+ handleTouchMove
618
+ ]);
405
619
  const handleTouchStart = (0, react.useCallback)((e) => {
406
620
  if (!draggable || e.touches.length !== 1) return;
407
621
  containerRef.current?.focus();
@@ -411,13 +625,14 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
411
625
  touchStartXRef.current = e.touches[0].clientX;
412
626
  touchStartYRef.current = e.touches[0].clientY;
413
627
  velocityRef.current = 0;
414
- }, [draggable]);
628
+ attachTouchDragListeners();
629
+ }, [attachTouchDragListeners, draggable]);
415
630
  const handleWheel = (0, react.useCallback)((e) => {
416
- if (!wheelEnabled) return;
631
+ if (!wheelEnabled) return false;
417
632
  const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
418
- if (!currentContainer) return;
633
+ if (!currentContainer) return false;
419
634
  const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
420
- if (ownedDelta === null) return;
635
+ if (ownedDelta === null) return false;
421
636
  e.preventDefault();
422
637
  const wheelDelta = -ownedDelta;
423
638
  const nextPosition = wrapPosition(positionRef.current + wheelDelta * WHEEL_IMMEDIATE_DELTA_FACTOR);
@@ -426,125 +641,155 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
426
641
  velocityRef.current = Math.max(-150, Math.min(MAX_VELOCITY, velocityRef.current));
427
642
  pauseAutoScroll();
428
643
  scheduleAutoScrollResume();
644
+ startAnimationLoop();
645
+ return true;
429
646
  }, [
430
647
  commitPosition,
431
648
  isHorizontal,
432
649
  pauseAutoScroll,
433
650
  scheduleAutoScrollResume,
651
+ startAnimationLoop,
434
652
  wheelEnabled,
435
653
  wheelMode,
436
654
  wrapPosition
437
655
  ]);
438
- const handleEdgeHoverMove = (0, react.useCallback)((e) => {
439
- if (!edgeHoverScroll) return;
656
+ const applyEdgeHoverPoint = (0, react.useCallback)((point) => {
657
+ if (!edgeHoverScroll) return false;
440
658
  const viewport = containerRef.current?.parentElement ?? containerRef.current;
441
- if (!viewport) return;
442
- const visibleInterval = getVisibleEdgeInterval(viewport.getBoundingClientRect(), isHorizontal);
659
+ if (!viewport) return false;
660
+ const visibleInterval = readVisibleEdgeInterval(viewport);
443
661
  if (!visibleInterval) {
444
- edgeHoverDirectionRef.current = 0;
445
- return;
446
- }
447
- const pointerPosition = isHorizontal ? e.clientX : e.clientY;
448
- const effectiveEdgeHoverSize = Math.min(normalizedEdgeHoverSize, visibleInterval.size / 2);
449
- if (pointerPosition <= visibleInterval.start + effectiveEdgeHoverSize) {
450
- edgeHoverDirectionRef.current = 1;
451
- return;
452
- }
453
- if (pointerPosition >= visibleInterval.end - effectiveEdgeHoverSize) {
454
- edgeHoverDirectionRef.current = -1;
455
- return;
662
+ setEdgeHoverIntensity(0);
663
+ return false;
456
664
  }
457
- edgeHoverDirectionRef.current = 0;
665
+ const nextIntensity = getEdgeHoverIntensity(isHorizontal ? point.clientX : point.clientY, visibleInterval, normalizedEdgeHoverSize);
666
+ setEdgeHoverIntensity(nextIntensity);
667
+ return true;
458
668
  }, [
459
669
  edgeHoverScroll,
460
670
  isHorizontal,
461
- normalizedEdgeHoverSize
671
+ normalizedEdgeHoverSize,
672
+ readVisibleEdgeInterval,
673
+ setEdgeHoverIntensity
462
674
  ]);
675
+ const handleEdgeHover = (0, react.useCallback)((point) => {
676
+ const accepted = applyEdgeHoverPoint(point);
677
+ externalEdgeHoverLeaseRef.current = accepted;
678
+ return accepted;
679
+ }, [applyEdgeHoverPoint]);
680
+ const clearEdgeHover = (0, react.useCallback)(() => {
681
+ externalEdgeHoverLeaseRef.current = false;
682
+ setEdgeHoverIntensity(0);
683
+ }, [setEdgeHoverIntensity]);
684
+ (0, react.useImperativeHandle)(ref, () => ({
685
+ clearEdgeHover,
686
+ handleEdgeHover,
687
+ handleWheel
688
+ }), [
689
+ clearEdgeHover,
690
+ handleEdgeHover,
691
+ handleWheel
692
+ ]);
693
+ const handleEdgeHoverMove = (0, react.useCallback)((e) => {
694
+ if (externalEdgeHoverLeaseRef.current) return;
695
+ applyEdgeHoverPoint(e);
696
+ }, [applyEdgeHoverPoint]);
463
697
  const handleEdgeHoverLeave = (0, react.useCallback)(() => {
464
- edgeHoverDirectionRef.current = 0;
465
- }, []);
698
+ if (externalEdgeHoverLeaseRef.current) return;
699
+ setEdgeHoverIntensity(0);
700
+ }, [setEdgeHoverIntensity]);
466
701
  (0, react.useEffect)(() => {
467
702
  const currentContainer = containerRef.current;
468
703
  if (!currentContainer) return;
469
- currentContainer.addEventListener("mousedown", handleMouseDown);
470
- window.addEventListener("mousemove", handleMouseMove);
471
- window.addEventListener("mouseup", handleMouseUp);
472
704
  if (draggable) {
705
+ currentContainer.addEventListener("mousedown", handleMouseDown);
473
706
  currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
474
- window.addEventListener("touchmove", handleTouchMove, { passive: false });
475
- window.addEventListener("touchend", handleTouchEnd, { passive: true });
476
707
  }
477
708
  if (wheelEnabled) currentContainer.addEventListener("wheel", handleWheel, { passive: false });
478
709
  const edgeHoverTarget = currentContainer.parentElement ?? currentContainer;
479
- edgeHoverTarget.addEventListener("mousemove", handleEdgeHoverMove);
480
- edgeHoverTarget.addEventListener("mouseleave", handleEdgeHoverLeave);
710
+ const usesPointerEdgeHover = typeof window.PointerEvent === "function";
711
+ const usesNativeEdgeHover = edgeHoverScroll && edgeHoverInputMode === "hybrid";
712
+ if (usesNativeEdgeHover && usesPointerEdgeHover) {
713
+ edgeHoverTarget.addEventListener("pointermove", handleEdgeHoverMove);
714
+ edgeHoverTarget.addEventListener("pointerleave", handleEdgeHoverLeave);
715
+ edgeHoverTarget.addEventListener("pointercancel", clearEdgeHover);
716
+ } else if (usesNativeEdgeHover) {
717
+ edgeHoverTarget.addEventListener("mousemove", handleEdgeHoverMove);
718
+ edgeHoverTarget.addEventListener("mouseleave", handleEdgeHoverLeave);
719
+ }
481
720
  return () => {
482
- currentContainer.removeEventListener("mousedown", handleMouseDown);
483
- window.removeEventListener("mousemove", handleMouseMove);
484
- window.removeEventListener("mouseup", handleMouseUp);
485
721
  if (draggable) {
722
+ currentContainer.removeEventListener("mousedown", handleMouseDown);
486
723
  currentContainer.removeEventListener("touchstart", handleTouchStart);
487
- window.removeEventListener("touchmove", handleTouchMove);
488
- window.removeEventListener("touchend", handleTouchEnd);
489
724
  }
490
725
  if (wheelEnabled) currentContainer.removeEventListener("wheel", handleWheel);
491
- edgeHoverTarget.removeEventListener("mousemove", handleEdgeHoverMove);
492
- edgeHoverTarget.removeEventListener("mouseleave", handleEdgeHoverLeave);
726
+ if (usesNativeEdgeHover && usesPointerEdgeHover) {
727
+ edgeHoverTarget.removeEventListener("pointermove", handleEdgeHoverMove);
728
+ edgeHoverTarget.removeEventListener("pointerleave", handleEdgeHoverLeave);
729
+ edgeHoverTarget.removeEventListener("pointercancel", clearEdgeHover);
730
+ } else if (usesNativeEdgeHover) {
731
+ edgeHoverTarget.removeEventListener("mousemove", handleEdgeHoverMove);
732
+ edgeHoverTarget.removeEventListener("mouseleave", handleEdgeHoverLeave);
733
+ }
734
+ detachMouseDragListeners();
735
+ detachTouchDragListeners();
493
736
  };
494
737
  }, [
738
+ detachMouseDragListeners,
739
+ detachTouchDragListeners,
495
740
  draggable,
741
+ edgeHoverScroll,
742
+ edgeHoverInputMode,
743
+ clearEdgeHover,
496
744
  handleEdgeHoverLeave,
497
745
  handleEdgeHoverMove,
498
746
  handleMouseDown,
499
- handleMouseMove,
500
- handleMouseUp,
501
- handleTouchEnd,
502
- handleTouchMove,
503
747
  handleTouchStart,
504
748
  handleWheel,
505
749
  wheelEnabled
506
750
  ]);
507
- const debouncedRecalculate = (0, react.useCallback)(() => {
508
- if (resizeDebounceTimerRef.current) clearTimeout(resizeDebounceTimerRef.current);
509
- resizeDebounceTimerRef.current = setTimeout(() => {
510
- resizeDebounceTimerRef.current = null;
511
- recalculateLoopPoint();
512
- }, RESIZE_DEBOUNCE_MS);
513
- }, [recalculateLoopPoint]);
514
751
  (0, react.useEffect)(() => {
515
- window.addEventListener("resize", debouncedRecalculate);
752
+ window.addEventListener("resize", scheduleLoopPointRecalculation);
516
753
  return () => {
517
- window.removeEventListener("resize", debouncedRecalculate);
754
+ window.removeEventListener("resize", scheduleLoopPointRecalculation);
518
755
  };
519
- }, [debouncedRecalculate]);
756
+ }, [scheduleLoopPointRecalculation]);
520
757
  (0, react.useEffect)(() => {
521
- if (!containerRef.current || typeof ResizeObserver === "undefined") return;
758
+ if (!containerRef.current || !originalGroupRef.current || typeof ResizeObserver === "undefined") return;
759
+ const currentContainer = containerRef.current;
760
+ const originalGroup = originalGroupRef.current;
761
+ const viewport = currentContainer.parentElement ?? currentContainer;
522
762
  const observer = new ResizeObserver(() => {
523
- debouncedRecalculate();
763
+ scheduleLoopPointRecalculation();
524
764
  });
525
- observer.observe(containerRef.current);
765
+ observer.observe(originalGroup);
766
+ if (viewport !== originalGroup) observer.observe(viewport);
526
767
  return () => {
527
768
  observer.disconnect();
528
769
  };
529
- }, [debouncedRecalculate]);
770
+ }, [scheduleLoopPointRecalculation]);
530
771
  (0, react.useEffect)(() => {
531
772
  return () => {
532
- if (resizeDebounceTimerRef.current) {
533
- clearTimeout(resizeDebounceTimerRef.current);
534
- resizeDebounceTimerRef.current = null;
773
+ if (loopMeasurementFrameRef.current !== null) {
774
+ cancelAnimationFrame(loopMeasurementFrameRef.current);
775
+ loopMeasurementFrameRef.current = null;
535
776
  }
777
+ clearEdgeHoverIntervalCache();
536
778
  };
537
- }, []);
779
+ }, [clearEdgeHoverIntervalCache]);
538
780
  (0, react.useEffect)(() => {
539
781
  const handleVisibilityChange = () => {
540
782
  setIsTabActive(!document.hidden);
541
- if (!document.hidden) lastFrameTimeRef.current = performance.now();
783
+ if (!document.hidden) {
784
+ lastFrameTimeRef.current = performance.now();
785
+ startAnimationLoop();
786
+ }
542
787
  };
543
788
  document.addEventListener("visibilitychange", handleVisibilityChange);
544
789
  return () => {
545
790
  document.removeEventListener("visibilitychange", handleVisibilityChange);
546
791
  };
547
- }, []);
792
+ }, [startAnimationLoop]);
548
793
  (0, react.useEffect)(() => {
549
794
  return () => {
550
795
  clearInactivityTimer();
@@ -552,10 +797,11 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
552
797
  }, [clearInactivityTimer]);
553
798
  (0, react.useEffect)(() => {
554
799
  if (!isTabActive || isPaused) {
555
- if (animationFrameRef.current) {
800
+ if (animationFrameRef.current !== null) {
556
801
  cancelAnimationFrame(animationFrameRef.current);
557
802
  animationFrameRef.current = null;
558
803
  }
804
+ animationLoopRef.current = null;
559
805
  return;
560
806
  }
561
807
  lastFrameTimeRef.current = 0;
@@ -569,27 +815,43 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
569
815
  else velocityRef.current = 0;
570
816
  let nextPosition = positionRef.current;
571
817
  const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
572
- const edgeHoverMultiplier = edgeHoverScroll ? edgeHoverDirectionRef.current : directionMultiplier;
573
- if (autoScrollRef.current.desired && autoScrollRef.current.active && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0)) nextPosition += edgeHoverMultiplier * effectiveSpeed * deltaTime;
574
- if (!isDraggingRef.current && Math.abs(velocityRef.current) > .1) nextPosition += velocityRef.current * deltaTime;
818
+ const edgeHoverMultiplier = edgeHoverScroll ? getEdgeHoverVelocityScale(edgeHoverIntensityRef.current, normalizedEdgeHoverSpeedMultiplier) : directionMultiplier;
819
+ const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
820
+ if (shouldAutoScroll) {
821
+ const autoScrollDeltaTime = edgeHoverScroll ? Math.min(deltaTime, MAX_EDGE_HOVER_DELTA_TIME) : deltaTime;
822
+ nextPosition += edgeHoverMultiplier * effectiveSpeed * autoScrollDeltaTime;
823
+ }
824
+ const shouldApplyMomentum = !isDraggingRef.current && Math.abs(velocityRef.current) > .1;
825
+ if (shouldApplyMomentum) nextPosition += velocityRef.current * deltaTime;
575
826
  nextPosition = wrapPosition(nextPosition);
576
827
  commitPosition(nextPosition);
577
- animationFrameRef.current = requestAnimationFrame(animate);
828
+ if (isDraggingRef.current || shouldAutoScroll || shouldApplyMomentum) {
829
+ animationFrameRef.current = requestAnimationFrame(animate);
830
+ return;
831
+ }
832
+ animationFrameRef.current = null;
833
+ lastFrameTimeRef.current = 0;
578
834
  };
579
- animationFrameRef.current = requestAnimationFrame(animate);
835
+ animationLoopRef.current = animate;
836
+ startAnimationLoop();
580
837
  return () => {
581
- if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
838
+ if (animationFrameRef.current !== null) {
839
+ cancelAnimationFrame(animationFrameRef.current);
840
+ animationFrameRef.current = null;
841
+ }
842
+ if (animationLoopRef.current === animate) animationLoopRef.current = null;
582
843
  };
583
844
  }, [
584
845
  commitPosition,
585
846
  direction,
586
847
  edgeHoverScroll,
587
- isHorizontal,
588
848
  isPaused,
589
849
  isTabActive,
590
850
  normalizedFriction,
851
+ normalizedEdgeHoverSpeedMultiplier,
591
852
  normalizedSpeed,
592
853
  prefersReducedMotion,
854
+ startAnimationLoop,
593
855
  wrapPosition
594
856
  ]);
595
857
  (0, react.useEffect)(() => {
@@ -601,7 +863,7 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
601
863
  }, {
602
864
  root: null,
603
865
  rootMargin: lazyRootMargin,
604
- threshold: lazyThreshold
866
+ threshold: normalizedLazyThreshold
605
867
  });
606
868
  const items = containerRef.current.querySelectorAll(".content-item");
607
869
  items.forEach((item) => observer.observe(item));
@@ -613,13 +875,14 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
613
875
  children,
614
876
  lazy,
615
877
  lazyRootMargin,
616
- lazyThreshold
878
+ normalizedLazyThreshold
617
879
  ]);
618
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
619
- className: "react-sway-container scroller-content",
880
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
881
+ "aria-label": ariaLabel,
882
+ className: `react-sway-container scroller-content${className ? ` ${className}` : ""}`,
620
883
  ref: containerRef,
621
884
  style: {
622
- cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
885
+ cursor: draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
623
886
  display: isHorizontal ? "flex" : void 0,
624
887
  flexDirection: isHorizontal ? "row" : void 0,
625
888
  height: isHorizontal ? "100%" : "max-content",
@@ -652,14 +915,18 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
652
915
  "aria-hidden": "true",
653
916
  className: "content-group duplicate",
654
917
  "data-duplicate": "true",
918
+ inert: true,
655
919
  role: "presentation",
656
920
  style: isHorizontal ? {
657
921
  display: "flex",
658
- flex: "0 0 auto"
659
- } : void 0,
922
+ flex: "0 0 auto",
923
+ pointerEvents: "none"
924
+ } : { pointerEvents: "none" },
660
925
  children
661
926
  }, `duplicate-${duplicateIndex}`))]
662
927
  });
663
- }
928
+ });
664
929
  //#endregion
930
+ exports.DEFAULT_EDGE_HOVER_SIZE = DEFAULT_EDGE_HOVER_SIZE;
931
+ exports.DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER = DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER;
665
932
  exports.ReactSway = ReactSway;