Provides a reusable `requestAnimationFrame` position engine for endless-scroll marquee animations, decoupled from any specific DOM strategy (scroll position or CSS transform). ## Key Components ### `MarqueeEngineOptions` (interface) Configuration object passed to the hook: | Option | Description | |---|---| | `active` | Master switch; enables/disables the rAF loop | | `speed` | Cruise speed in px/s | | `isPaused` | Per-frame pause verdict function (caller-owned reason set) | | `getWrapSize` | Returns the single-copy width/height for seam wrapping | | `getWrapMin` | Optional lower bound of the wrap range (default `0`) | | `apply` | Writes position to the DOM (`scrollLeft` or `transform`) | | `readBack` | Reads externally mutated position (user drag, native scroll) | | `onAfterFrame` | Called after each frame (e.g. hover re-resolution) | ### `MarqueeEngineHandle` (interface) Returned by the hook: | Member | Description | |---|---| | `posRef` | Float position ref in px; callers may write during seam warps | | `glideBy(px)` | Accumulates a signed glide distance for chevron navigation | | `wrap(pos)` | Utility to normalize any position into `[0, wrapSize)` | ### `useMarqueeEngine(options)` (hook) Core export. Manages three internal refs (`posRef`, `glideRemainingRef`, `speedEnvRef`), a latest-options ref to avoid restarting the rAF loop on every render, and the main `tick` function handling: - **Velocity envelope** — ~250ms ease between `0` and `speed` (smooth pause/resume) - **Glide mode** — proportional ease-out for chevron navigation, wrapping modulo copy size - **Cruise mode** — steady advance at enveloped speed with external resync - **Idle mode** — follows external scroll position so resume starts from current location ## Usage Example ```typescript // CardsStrip (horizontal scroller driver) const engine = useMarqueeEngine({ active: isVisible, speed: 80, isPaused: (now) => pauseReasons.size > 0 || suppressUntil > now, getWrapSize: () => trackRef.current?.scrollWidth / 2 ?? 0, getWrapMin: () => SEAM_BUFFER, apply: (pos) => { if (trackRef.current) trackRef.current.scrollLeft = pos }, readBack: () => trackRef.current?.scrollLeft ?? 0, onAfterFrame: resolveHover, }) // Chevron navigation engine.glideBy(cardWidth) // MarqueeWall (transform driver — no readBack needed) const engine = useMarqueeEngine({ active: true, speed: 50, isPaused: () => !isVisible, getWrapSize: () => trackRef.current?.offsetWidth / 2 ?? 0, apply: (pos) => { if (trackRef.current) trackRef.current.style.transform = `translateX(${-pos}px)` }, }) ``` ## Source [`use-marquee-engine.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/use-marquee-engine.ts)