"use client"; /** * Infinite marquee whose speed + skew react to scroll velocity. * - Base drift via Motion `animationFrame` + a wrapped motion value. * - Scroll velocity (useVelocity over scrollY) adds directional boost + skew, * so the tape "leans" as you fling the page — a signature kinetic touch. * - Two copies of the content guarantee a seamless loop. * - prefers-reduced-motion: static row, no transform churn. */ import { useRef } from "react"; import { motion, useScroll, useVelocity, useSpring, useTransform, useMotionValue, useAnimationFrame, useReducedMotion, wrap, } from "motion/react"; export default function VelocityMarquee({ items, baseVelocity = 60, }: { items: string[]; baseVelocity?: number; }) { const reduce = useReducedMotion(); const baseX = useMotionValue(0); const { scrollY } = useScroll(); const scrollVelocity = useVelocity(scrollY); const smoothVelocity = useSpring(scrollVelocity, { damping: 50, stiffness: 400 }); const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 4], { clamp: false }); const skew = useTransform(smoothVelocity, [-2000, 0, 2000], [-6, 0, 6], { clamp: true }); // 50% because we render the content twice; wrap keeps it seamless. const x = useTransform(baseX, (v) => `${wrap(-50, 0, v)}%`); const directionFactor = useRef(1); useAnimationFrame((_, delta) => { if (reduce) return; let moveBy = directionFactor.current * baseVelocity * (delta / 1000); const vf = velocityFactor.get(); if (vf < 0) directionFactor.current = -1; else if (vf > 0) directionFactor.current = 1; moveBy += directionFactor.current * moveBy * vf; baseX.set(baseX.get() + moveBy / 14); }); const Row = ( <> {items.map((it, i) => ( {it} ))} ); if (reduce) { return ( ); } return ( ); }