agency-web/app/components/VelocityMarquee.tsx

80 lines
2.3 KiB
TypeScript

"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) => (
<span className="marq__item" key={i}>
{it}
<span className="marq__star" aria-hidden="true"></span>
</span>
))}
</>
);
if (reduce) {
return (
<div className="marq" aria-hidden="true">
<div className="marq__track marq__track--static">{Row}</div>
</div>
);
}
return (
<motion.div className="marq" style={{ skewX: skew }} aria-hidden="true">
<motion.div className="marq__track" style={{ x }}>
{Row}
{Row}
</motion.div>
</motion.div>
);
}