"use client"; import { useLayoutEffect, useRef, useState } from "react"; import { motion, useAnimationFrame, useMotionValue, useReducedMotion } from "framer-motion"; import { cn } from "@/lib/utils"; type MarqueeDirection = "left" | "right"; export type MarqueeRow = { items: string[]; direction: MarqueeDirection; duration?: number; }; type StackedMarqueeSectionProps = { className?: string; rows?: MarqueeRow[]; }; const DEFAULT_ROWS: MarqueeRow[] = [ { direction: "left", duration: 126, items: [ "Next.js", "React", "WordPress", "Docker", "Technical SEO", "Tailwind CSS", "Google Analytics", "Framer Motion", "Cloudflare", "Performance", ], }, { direction: "right", duration: 220, items: [ "TypeScript", "Node.js", "WooCommerce", "Schema Markup", "DevOps", "Responsive Design", "Core Web Vitals", "Plausible Analytics", "Design Systems", "Server Infrastructure", ], }, { direction: "left", duration: 168, items: [ "JavaScript", "REST APIs", "Git", "Linux", "Search Console", "Conversion Tracking", "UI Engineering", "Accessibility", "Component Architecture", "Web Performance", ], }, { direction: "right", duration: 144, items: [ "Frontend Strategy", "CMS Integration", "Motion Design", "Content Architecture", "E-Commerce", "SEO Audits", "Deployments", "Scalable UI", "Analytics Setup", "Product Pages", ], }, ]; function MarqueeRowTrack({ items, direction, duration = 24, }: MarqueeRow) { const shouldReduceMotion = useReducedMotion(); const viewportRef = useRef(null); const segmentRef = useRef(null); const [segmentWidth, setSegmentWidth] = useState(0); const [copyCount, setCopyCount] = useState(3); const x = useMotionValue(0); const isReady = shouldReduceMotion || segmentWidth > 0; useLayoutEffect(() => { const viewport = viewportRef.current; const segment = segmentRef.current; if (!viewport || !segment) { return; } const measure = () => { const nextSegmentWidth = segment.getBoundingClientRect().width; const viewportWidth = viewport.clientWidth; if (!nextSegmentWidth || !viewportWidth) { return; } setSegmentWidth(nextSegmentWidth); setCopyCount(Math.max(3, Math.ceil(viewportWidth / nextSegmentWidth) + 2)); x.set(direction === "right" ? -nextSegmentWidth : 0); }; measure(); const observer = new ResizeObserver(() => { measure(); }); observer.observe(viewport); observer.observe(segment); return () => { observer.disconnect(); }; }, [direction, items, x]); useAnimationFrame((_, delta) => { if (shouldReduceMotion || segmentWidth === 0) { return; } const distancePerMs = segmentWidth / (duration * 1000); const offset = distancePerMs * delta; const current = x.get(); if (direction === "right") { const next = current + offset; x.set(next >= 0 ? next - segmentWidth : next); return; } const next = current - offset; x.set(next <= -segmentWidth ? next + segmentWidth : next); }); return (
{Array.from({ length: copyCount }).map((_, copyIndex) => (
0} dir="ltr" > {items.map((item, itemIndex) => (
{item}
))}
))}
); } export function StackedMarqueeSection({ className, rows = DEFAULT_ROWS, }: StackedMarqueeSectionProps) { return (
{rows.map((row, index) => (
))}
); }