"use client"; import { useEffect, useRef, useState, type CSSProperties, type RefObject } from "react"; import { motion, useMotionValueEvent, useReducedMotion, useScroll, useSpring } from "motion/react"; type TimelineFlowProps = { /** Events in order, oldest or first step on top. */ items: { title: string; time?: string; description?: string }[]; /** Scroll box to watch. Leave out to follow the page. */ container?: RefObject; /** Rail and dot colour. */ accent?: string; /** Wrapper look. */ className?: string; }; const countLit = (marks: number[], progress: number) => marks.filter((mark) => mark <= progress).length; /* TimelineFlow: a rail fills as you scroll, and each event lights up the moment the fill reaches its dot. */ export function TimelineFlow({ items, container, accent = "#a78bfa", className = "" }: TimelineFlowProps) { const reduce = useReducedMotion(); const list = useRef(null); const marks = useRef([]); const [lit, setLit] = useState(0); const { scrollYProgress } = useScroll({ target: list, container, offset: ["start 0.9", "end end"] }); const fill = useSpring(scrollYProgress, { stiffness: 140, damping: 28 }); useMotionValueEvent(fill, "change", (progress) => setLit(countLit(marks.current, progress))); // Each dot's place along the rail, kept fresh as text wraps on resize. useEffect(() => { const box = list.current; if (!box) return; const measure = () => { marks.current = [...box.children].map((el) => (el as HTMLElement).offsetTop / box.offsetHeight); setLit(countLit(marks.current, fill.get())); }; const observer = new ResizeObserver(measure); observer.observe(box); return () => observer.disconnect(); }, [fill]); return (
    {items.map((item, i) => { const on = reduce || i < lit; return (
  1. {item.time &&

    {item.time}

    }

    {item.title}

    {item.description &&

    {item.description}

    }
  2. ); })}
); }