import { useEffect, useRef, type CSSProperties, type ReactNode, } from 'react'; import { prefersReducedMotion } from './utils.js'; export interface ParallaxProps { /** Slot moved on scroll. */ children: ReactNode; /** * Translation speed multiplier. 0 = stationary, 1 = scrolls with the * page (default), 0.5 = scrolls half-speed, -0.5 = scrolls upward. * Sensible range: −1 to 1. */ speed?: number; /** Translation axis. Default `'y'`. */ axis?: 'y' | 'x'; /** Bypass entirely — useful for tests / reduced-motion. */ disableMotion?: boolean; className?: string; style?: CSSProperties; } /** * `` — scroll-driven translation wrapper. * * Uses `transform: translate3d(…)` updated inside a `requestAnimationFrame` * loop driven by `window.scroll`. The transform is computed relative to * the element's vertical centre crossing the viewport's centre — so the * effect peaks while the element is on-screen and never accumulates * unbounded translation. * * Honours `prefers-reduced-motion`: with `disableMotion` (or the OS * setting) the wrapper renders un-transformed. * * Wave 13.D.1. Zero deps, ~1.4 KB gzipped. Pairs with `` * and `` for the spatial-hero pattern. */ export function Parallax({ children, speed = 0.3, axis = 'y', disableMotion, className, style, }: ParallaxProps) { const ref = useRef(null); const reduced = disableMotion ?? prefersReducedMotion(); useEffect(() => { if (reduced) return; const el = ref.current; if (!el) return; let raf = 0; const apply = () => { raf = 0; const rect = el.getBoundingClientRect(); const vh = window.innerHeight || 1; // Distance, in px, of the element's centre from viewport centre. const delta = rect.top + rect.height / 2 - vh / 2; const t = -delta * speed; el.style.transform = axis === 'y' ? `translate3d(0, ${t.toFixed(2)}px, 0)` : `translate3d(${t.toFixed(2)}px, 0, 0)`; }; const onScroll = () => { if (raf) return; raf = window.requestAnimationFrame(apply); }; apply(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (raf) window.cancelAnimationFrame(raf); el.style.transform = ''; }; }, [reduced, speed, axis]); return (
{children}
); }