"use client"; import { useRef } from "react"; import { motion, useAnimationFrame, useMotionValue, useReducedMotion, useScroll, useSpring, useTransform, useVelocity, } from "motion/react"; type VelocityTextProps = { /** Text to loop. Make it long enough to fill at least a third of the row. */ children: string; className?: string; /** Resting speed, in % of one copy per second. Negative runs the other way. */ baseVelocity?: number; }; const wrap = (min: number, max: number, v: number) => { const range = max - min; return ((((v - min) % range) + range) % range) + min; }; /* VelocityText: a looping text band that speeds up and leans when the page scrolls, and turns round when the scroll does. */ export function VelocityText({ children, className = "", baseVelocity = 4 }: VelocityTextProps) { const reduce = useReducedMotion(); const baseX = useMotionValue(0); const { scrollY } = useScroll(); const velocity = useSpring(useVelocity(scrollY), { damping: 50, stiffness: 400 }); const boost = useTransform(velocity, [0, 1000], [0, 5], { clamp: false }); const skewX = useTransform(velocity, [-1500, 0, 1500], [12, 0, -12]); // Four copies in the track; moving by 25% = one copy, so the loop is seamless. const x = useTransform(baseX, (v) => `${wrap(-25, 0, v)}%`); const direction = useRef(1); useAnimationFrame((_, delta) => { if (reduce) return; const b = boost.get(); if (b < 0) direction.current = -1; else if (b > 0) direction.current = 1; baseX.set(baseX.get() - direction.current * baseVelocity * (delta / 1000) * (1 + Math.abs(b))); }); return (
{children} {[0, 1, 2, 3].map((i) => ( {children} ))}
); }