"use client"; import { useRef, type ReactNode } from "react"; import { motion, useMotionValue, useReducedMotion, useScroll, useSpring, useTransform, type MotionValue } from "motion/react"; export type ParallaxStageLayer = { id: string; content: ReactNode; /** 0 stays still, 1 moves the full strength. Below 0 moves with the pointer, above 0 against it. */ depth: number; /** Size the layer past the stage edge so moving never shows a gap, for example "-inset-10". */ className?: string; }; type ParallaxStageProps = { /** Back layer first. */ layers: ParallaxStageLayer[]; /** Pixels a layer of depth 1 moves from the middle. */ strength?: number; /** Layers also move as the stage passes through the screen. This is what touch users see. */ scroll?: boolean; className?: string; }; type LayerProps = { layer: ParallaxStageLayer; strength: number; x: MotionValue; y: MotionValue; page: MotionValue; }; const FOLLOW = { stiffness: 90, damping: 18 }; function Layer({ layer, strength, x, y, page }: LayerProps) { const shiftX = useTransform(x, (v) => -v * layer.depth * strength); const shiftY = useTransform([y, page], ([py, ps]: number[]) => -(py + ps) * layer.depth * strength); return ( {layer.content} ); } /* ParallaxStage: layers at different depths slide apart under the mouse and as the stage scrolls by, so a flat scene opens up. */ export function ParallaxStage({ layers, strength = 40, scroll = true, className = "" }: ParallaxStageProps) { const reduce = useReducedMotion(); const box = useRef(null); const x = useSpring(0, FOLLOW); const y = useSpring(0, FOLLOW); const still = useMotionValue(0); const { scrollYProgress } = useScroll({ target: box, offset: ["start end", "end start"] }); const passing = useTransform(scrollYProgress, (v) => (scroll ? v * 2 - 1 : 0)); const onMove = (e: React.PointerEvent) => { if (reduce || e.pointerType === "touch") return; const r = e.currentTarget.getBoundingClientRect(); x.set(((e.clientX - r.left) / r.width) * 2 - 1); y.set(((e.clientY - r.top) / r.height) * 2 - 1); }; const onLeave = () => { x.set(0); y.set(0); }; return (
{layers.map((layer) => ( ))}
); }