react-sway 0.4.2 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-11-09
4
+
5
+ - Add perspective depth scrolling with toward/away directions and configurable camera distance and plane spacing.
6
+ - Mount each depth child once and recycle only at zero opacity without crossing the camera.
7
+ - Share wheel, drag, touch, keyboard, pause, and lifecycle behavior across all three axes.
8
+ - Add `active` and `reducedMotion` for host lifecycle and accessibility integration; preserve React 17/18/19 and existing planar behavior.
9
+ - Add responsive depth documentation and browser coverage, plus geometry and lifecycle regressions.
10
+ - Make the ESM build directly importable in Node with React 17, and verify packed ESM/CJS/SSR consumers on React 17/18/19.
11
+ - Update development dependencies and npm, test Node 22/24/26, and resolve all known dependency audit findings. Keep TypeScript 6.0.3 for ESLint parser compatibility.
12
+
13
+ ## 0.4.3 - 2026-10-21
14
+
15
+ - Preserve browser zoom gestures, nested controls, and secondary mouse clicks.
16
+ - Stop external edge-hover motion when edge-hover mode is disabled.
17
+ - Preserve inert duplicate groups on React 17 and 18 as well as React 19.
18
+ - Update the development toolchain; keep TypeScript 6 for parser compatibility.
19
+
3
20
  ## 0.4.0
4
21
 
5
22
  - Added horizontal and vertical axis semantics with page-safe touch behavior.
package/README.md CHANGED
@@ -1,22 +1,23 @@
1
1
  # react-sway
2
2
 
3
- A React component for smooth, infinite, and interactive content scrolling on the vertical or horizontal axis. It duplicates content to create a seamless looping effect, controllable via touch, mouse drag, wheel, keyboard, or edge hover.
3
+ A React component for smooth, infinite, and interactive content scrolling on vertical, horizontal, or perspective depth axes, controllable via touch, mouse drag, wheel, keyboard, or edge hover. React 17, 18, and 19 are supported without runtime dependencies beyond React and React DOM.
4
4
 
5
5
  ## What is `react-sway`?
6
6
 
7
7
  React Sway takes your list of items and makes them scroll endlessly. It's designed to be easy to use and performant, with auto-scrolling that pauses when users interact. Edge-hover layouts can also be configured to scroll only while the pointer hovers the active axis boundary: top/bottom for vertical loops or left/right for horizontal loops. Edge-hover speed ramps from idle to a bounded multiple of the configured speed as the pointer moves deeper into the boundary zone.
8
8
 
9
- It works by duplicating your content to create a seamless loop and uses CSS transforms for smooth animation. The duplicated content is wrapped in inert `<aside>` elements with `aria-hidden="true"` and `role="presentation"` so assistive technology, pointer input, and search crawlers keep treating the original group as canonical.
9
+ Vertical and horizontal modes duplicate content into inert `<aside>` elements with `aria-hidden="true"` and `role="presentation"`. Depth mode mounts each direct child once and recycles its 3D plane at zero opacity, keeping every plane behind the perspective camera.
10
10
 
11
11
  ### Core Features
12
12
 
13
- * **Smooth Infinite Scroll:** Content loops continuously on the vertical or horizontal axis.
13
+ * **Smooth Infinite Scroll:** Content loops continuously on vertical, horizontal, or depth axes.
14
14
  * **Auto-Scroll:** Scrolls automatically, with configurable speed and direction.
15
15
  * **Edge Hover:** Use `edgeHoverScroll` to keep auto-scroll idle until the pointer enters the active axis boundary zone, with speed scaled smoothly by boundary depth and capped by `edgeHoverSpeedMultiplier`.
16
16
  * **User Friendly Interactions:**
17
17
  * Click and drag to scroll.
18
18
  * Swipe on touch devices.
19
- * Mouse wheel support with axis-aware intent detection, delta-mode normalization, and velocity capping.
19
+ * Mouse wheel support with axis-aware intent detection, delta-mode normalization, and velocity capping. Browser zoom gestures pass through.
20
+ * Nested buttons, links, and editable fields retain their native interactions.
20
21
  * Keyboard controls: Spacebar to pause/resume, Arrow keys to scroll, Home/End to jump.
21
22
  * **External Input Bridge:** Use `ref` with `ReactSwayHandle` to route wheel and edge-hover intent from a parent drag/drop surface into Sway.
22
23
  * **Idle-Safe Listeners:** Global drag listeners are attached only during active drag or touch gestures and cleaned up on release, cancel, blur, and unmount.
@@ -276,30 +277,66 @@ function MinimalEdgeHoverExamples() {
276
277
  }
277
278
  ```
278
279
 
280
+ ### Perspective depth scrolling
281
+
282
+ ```tsx
283
+ import { ReactSway } from 'react-sway';
284
+
285
+ function DepthGallery({ active = true }: { active?: boolean }) {
286
+ return (
287
+ <div className="depth-viewport">
288
+ <ReactSway active={active} ariaLabel="Photo gallery" axis="depth" direction="toward">
289
+ <img alt="Mountain landscape" className="depth-image" src="/mountain.jpg" />
290
+ <img alt="Modern architecture" className="depth-image depth-image-offset" src="/building.jpg" />
291
+ <img alt="Coastal landscape" className="depth-image" src="/coast.jpg" />
292
+ </ReactSway>
293
+ </div>
294
+ );
295
+ }
296
+ ```
297
+
298
+ ```css
299
+ .depth-viewport { height: 600px; overflow: hidden; position: relative; }
300
+ .depth-image { display: block; height: 200px; max-width: none; object-fit: cover; transform: translate(-50%, -50%); width: 160px; }
301
+ .depth-image-offset { margin-left: 50px; margin-top: -80px; width: 200px; }
302
+ ```
303
+
304
+ Each **direct child** is one plane anchored at the viewport center. A fragment or a list wrapper is one plane; pass separate children for independent motion. Give children explicit sizes and use their CSS for composition, offsets, and tilt. The positioned parent supplies viewport dimensions and clipping. Keep `overflow: hidden` on that parent so the internal 3D scene retains depth.
305
+
306
+ `direction="toward"` approaches the camera; `"away"` recedes. Either direction infers `axis="depth"` when the axis is omitted. `depthPerspective` defaults to 800 CSS pixels and `depthSpacing` to 420; both must be finite and positive. Invalid values use their defaults, positive values below 1 are clamped to 1, and larger values are bounded by `Number.MAX_SAFE_INTEGER`. The exported `DEFAULT_DEPTH_PERSPECTIVE` and `DEFAULT_DEPTH_SPACING` constants share those defaults.
307
+
308
+ The loop period is the direct-child count times `depthSpacing`. Automatic movement and manual wheel/drag/keyboard input use the same periodic phase; vertical gestures and up/down arrows control depth. Set `autoScroll={false}` for manual exploration, or `active={false}` to suspend Sway's animation and input while preserving the current position. Empty depth scenes stay idle. Host applications can request reduced motion with `reducedMotion`; the system preference always takes precedence. Depth auto-scroll and momentum stop under reduced motion, while deliberate manual steps remain available. Existing planar loops retain their reduced speed behavior.
309
+
310
+ For decorative backgrounds, disable `keyboard`, `draggable`, and `wheelEnabled`, use empty image alt text, and put stationary application controls outside the Sway region. Automatic motion should have an accessible pause control. Inspect the [live depth example](https://lafittemehdy.github.io/sway/) for a responsive composition.
311
+
279
312
  ## Props
280
313
 
281
314
  | Prop | Type | Default | Description |
282
315
  |------|------|---------|-------------|
316
+ | `active` | `boolean` | `true` | Suspend Sway animation and input when false, retaining content and position. Nested native controls remain usable. |
283
317
  | `ariaLabel` | `string` | `'Scrollable content'` | Accessible name for the scrolling region. |
284
- | `axis` | `'horizontal' \| 'vertical'` | Inferred from `direction`, otherwise `'vertical'` | Scroll axis. Use `'horizontal'` for left/right loops. |
318
+ | `axis` | `'depth' \| 'horizontal' \| 'vertical'` | Inferred from `direction`, otherwise `'vertical'` | Scroll axis; depth assigns each direct child a 3D plane. |
285
319
  | `autoScroll` | `boolean` | `true` | Enable/disable auto-scrolling. |
286
320
  | `children` | `ReactNode` | - | Content elements to render in the scroll container. |
287
321
  | `className` | `string` | - | Additional class name applied to the semantic scrolling region. |
288
- | `direction` | `'down' \| 'left' \| 'right' \| 'up'` | `'up'` | Auto-scroll direction. `left` and `right` imply horizontal scrolling when `axis` is omitted. |
322
+ | `depthPerspective` | `number` | `800` | Perspective camera distance in CSS pixels, depth mode only. |
323
+ | `depthSpacing` | `number` | `420` | Distance between consecutive depth planes in CSS pixels. |
324
+ | `direction` | `'away' \| 'down' \| 'left' \| 'right' \| 'toward' \| 'up'` | `'up'` | `left`/`right` infer horizontal; `toward`/`away` infer depth. Explicit `axis` takes precedence. |
289
325
  | `draggable` | `boolean` | `true` | Enable mouse/touch drag interaction. |
290
326
  | `edgeHoverInputMode` | `'external' \| 'hybrid'` | `'hybrid'` | Use intrinsic pointer sensing plus the imperative bridge, or external imperative ownership only for composed drag surfaces. |
291
327
  | `edgeHoverScroll` | `boolean` | `false` | Only auto-scroll while hovering the active axis boundary: top/bottom for vertical, left/right for horizontal. A short ingress feather removes sensor chatter; velocity then accelerates through the physical edge and reaches its bounded cap one activation-zone width beyond it. |
292
328
  | `edgeHoverSize` | `number` | `96` | Thickness in pixels of each edge-hover activation zone. |
293
- | `edgeHoverSpeedMultiplier` | `number` | `6` | Maximum edge-hover velocity as a multiple of `speed`. The physical edge retains the established 3x response, then accelerates to the bounded `1`–`10` maximum beyond the viewport; set `1` for constant speed. |
329
+ | `edgeHoverSpeedMultiplier` | `number` | `6` | Maximum edge-hover velocity as a multiple of `speed`. The physical edge retains the established 3x response, then accelerates to the bounded `1`–`10` maximum beyond the viewport; set `1` for constant speed. |
294
330
  | `friction` | `number` | `0.95` | Momentum decay coefficient (0-1, lower = more friction). |
295
331
  | `keyboard` | `boolean` | `true` | Enable keyboard controls (Space, Arrow keys, Home/End). |
296
332
  | `lazy` | `boolean` | `true` | Enable lazy visibility detection via IntersectionObserver. |
297
333
  | `lazyRootMargin` | `string` | `'100px'` | IntersectionObserver `rootMargin` for lazy visibility detection. |
298
- | `lazyThreshold` | `number` | `0.01` | IntersectionObserver `threshold`, normalized to the inclusive `0`–`1` interval. |
334
+ | `lazyThreshold` | `number` | `0.01` | IntersectionObserver `threshold`, normalized to the inclusive `0`–`1` interval. |
299
335
  | `onPause` | `() => void` | - | Fired once when active autonomous scrolling transitions to interaction-paused. |
300
336
  | `onResume` | `() => void` | - | Fired when interaction-paused autonomous scrolling resumes. |
301
337
  | `onScroll` | `(position: number) => void` | - | Fired on every position change with the current scroll position. |
302
338
  | `pauseOnInteraction` | `boolean` | `true` | Pause auto-scroll during user interaction. |
339
+ | `reducedMotion` | `boolean` | `false` | Request reduced motion in addition to the OS preference. Stops depth auto-scroll and momentum. |
303
340
  | `resumeDelay` | `number` | `2000` | Milliseconds before auto-scroll resumes after interaction. |
304
341
  | `speed` | `number` | `0.5` | Baseline auto-scroll speed in pixels per frame at 60fps. Edge-hover mode preserves this baseline inside the active zone and adds bounded depth acceleration. |
305
342
  | `wheelEnabled` | `boolean` | `true` | Enable mouse wheel scrolling. |
@@ -373,11 +410,12 @@ External drag owners should retain pointer capture or route global pointer coord
373
410
 
374
411
  ## Runtime and accessibility contract
375
412
 
376
- - Duplicate loop groups are inert, `aria-hidden`, presentation-only, non-interactive, and pointer-disabled.
413
+ - Planar duplicate loop groups are inert, `aria-hidden`, presentation-only, non-interactive, and pointer-disabled. Depth children are never duplicated.
414
+ - Hidden pages pause animation and resume without consuming elapsed hidden time. Inactive instances also relinquish gesture ownership and discard momentum.
377
415
  - Mouse and touch global listeners are installed only during active drag gestures and are removed on release, cancel, blur, unmount, or prop changes.
378
416
  - Edge-hover mode does not continuously request animation frames while idle; it wakes when the pointer enters an edge, wheel input arrives, momentum exists, or auto-scroll resumes.
379
417
  - `wheelMode="axis"` lets cross-axis page scroll pass through. `wheelMode="capture"` intentionally lets Sway own every wheel event it receives.
380
418
 
381
419
  ## License
382
420
 
383
- This package is licensed under the [MIT License](../../LICENSE).
421
+ This package is licensed under the [MIT License](https://github.com/lafittemehdy/sway/blob/main/LICENSE).
@@ -4,8 +4,8 @@
4
4
  import { type ReactNode } from 'react';
5
5
  import { type SwayWheelMode } from './wheel-input';
6
6
  export type { SwayWheelMode } from './wheel-input';
7
- export type SwayAxis = 'horizontal' | 'vertical';
8
- export type SwayDirection = 'down' | 'left' | 'right' | 'up';
7
+ export type SwayAxis = 'depth' | 'horizontal' | 'vertical';
8
+ export type SwayDirection = 'away' | 'down' | 'left' | 'right' | 'toward' | 'up';
9
9
  export type SwayEdgeHoverInputMode = 'external' | 'hybrid';
10
10
  export interface ReactSwayHandle {
11
11
  /**
@@ -36,9 +36,11 @@ export interface ReactSwayHandle {
36
36
  * Props for the ReactSway infinite scrolling component.
37
37
  */
38
38
  export interface ReactSwayProps {
39
+ /** Suspend Sway animation and input while keeping content and scroll position mounted. @default true */
40
+ active?: boolean;
39
41
  /** Accessible name for the scrolling region. */
40
42
  ariaLabel?: string;
41
- /** Scroll axis. Horizontal directions imply `horizontal` when omitted. @default 'vertical' */
43
+ /** Scroll axis. Left/right imply horizontal; toward/away imply depth. @default 'vertical' */
42
44
  axis?: SwayAxis;
43
45
  /** Enable/disable auto-scrolling. @default true */
44
46
  autoScroll?: boolean;
@@ -46,6 +48,10 @@ export interface ReactSwayProps {
46
48
  children: ReactNode;
47
49
  /** Additional class name applied to the semantic scrolling region. */
48
50
  className?: string;
51
+ /** Camera distance in CSS pixels for depth mode. Invalid lengths use the default. @default 800 */
52
+ depthPerspective?: number;
53
+ /** Distance in CSS pixels between depth planes. Invalid lengths use the default. @default 420 */
54
+ depthSpacing?: number;
49
55
  /** Auto-scroll direction. @default 'up' */
50
56
  direction?: SwayDirection;
51
57
  /** Enable mouse/touch drag interaction. @default true */
@@ -83,6 +89,8 @@ export interface ReactSwayProps {
83
89
  onScroll?: (position: number) => void;
84
90
  /** Pause auto-scroll during user interaction. @default true */
85
91
  pauseOnInteraction?: boolean;
92
+ /** Request reduced motion in addition to the system preference; false never overrides the system. @default false */
93
+ reducedMotion?: boolean;
86
94
  /** Milliseconds before auto-scroll resumes after interaction. @default 2000 */
87
95
  resumeDelay?: number;
88
96
  /** Baseline auto-scroll speed in pixels per frame at 60fps. @default 0.5 */
@@ -106,11 +114,11 @@ export interface ReactSwayProps {
106
114
  * Renders children in a continuously looping scroll area with support for
107
115
  * auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
108
116
  * and optional edge-hover scrolling.
109
- * Content is duplicated to create a seamless loop effect. Duplicate content
117
+ * Planar content is duplicated to create a seamless loop effect. Duplicate content
110
118
  * is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
111
119
  *
112
- * Respects `prefers-reduced-motion: reduce` by lowering auto-scroll speed
113
- * and disabling momentum effects.
120
+ * Reduced motion stops depth auto-scroll, lowers planar auto-scroll speed,
121
+ * and disables momentum effects.
114
122
  *
115
123
  * @example
116
124
  * ```tsx
@@ -0,0 +1,9 @@
1
+ /** Maps one scroll phase onto periodic depth planes without crossing the perspective camera. */
2
+ export declare const DEFAULT_DEPTH_PERSPECTIVE = 800;
3
+ export declare const DEFAULT_DEPTH_SPACING = 420;
4
+ /** Bounds pixel lengths so periods and fade denominators remain finite and nonzero. */
5
+ export declare function normalizeDepthLength(value: number, fallback: number): number;
6
+ export declare function getDepthItemState(position: number, index: number, count: number, perspective?: number, spacing?: number): {
7
+ opacity: number;
8
+ z: number;
9
+ };