"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { motion, useAnimationFrame, useInView, useMotionValue, useReducedMotion } from "motion/react"; type InfiniteMarqueeProps = { /** One run of the strip. It repeats as often as the width needs. */ children: ReactNode; /** Pixels per second. */ speed?: number; direction?: "left" | "right"; /** Eases to a stop under the pointer or on keyboard focus. */ pauseOnHover?: boolean; /** Fades the two ends of the strip. */ fade?: boolean; /** Space between pieces in pixels. */ gap?: number; /** Name read out by screen readers. */ label?: string; className?: string; }; const MASK = "linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent)"; /* InfiniteMarquee: a strip that loops with no seam at any width. Slows to a stop on hover, stands still and scrolls by hand under reduced motion. */ export function InfiniteMarquee({ children, speed = 60, direction = "left", pauseOnHover = true, fade = true, gap = 32, label = "Scrolling strip", className = "", }: InfiniteMarqueeProps) { const reduce = useReducedMotion(); const box = useRef(null); const run = useRef(null); const seen = useInView(box); const [copies, setCopies] = useState(2); const x = useMotionValue(0); const runWidth = useRef(0); const travelled = useRef(0); const pace = useRef(speed); const held = useRef(false); useEffect(() => { if (!box.current || !run.current) return; const measure = () => { const full = box.current?.offsetWidth ?? 0; const one = run.current?.offsetWidth ?? 0; runWidth.current = one; if (one) setCopies(Math.ceil(full / one) + 1); }; const observer = new ResizeObserver(measure); observer.observe(box.current); observer.observe(run.current); return () => observer.disconnect(); }, []); useAnimationFrame((_, delta) => { const one = runWidth.current; if (reduce || !seen || !one) return; pace.current += ((held.current ? 0 : speed) - pace.current) * Math.min(1, delta / 160); travelled.current = (travelled.current + (pace.current * delta) / 1000) % one; x.set(direction === "left" ? -travelled.current : travelled.current - one); }); const hold = (on: boolean) => { if (pauseOnHover) held.current = on; }; const strip = (
{children}
); return (
hold(true)} onPointerLeave={() => hold(false)} onFocus={() => hold(true)} onBlur={() => hold(false)} className={`${reduce ? "overflow-x-auto" : "overflow-hidden"} ${className}`} style={fade ? { maskImage: MASK, WebkitMaskImage: MASK } : undefined} >
{strip}
{!reduce && Array.from({ length: copies - 1 }, (_, i) => (
{strip}
))}
); }