"use client"; import { motion, type Transition, useInView, useReducedMotion, useScroll, useSpring, } from "motion/react"; import { createContext, type ReactNode, type RefObject, useContext, useRef, } from "react"; import { EASE_OUT, SPRING_GLIDE, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type TimelineStatus = "done" | "current" | "upcoming"; type TimelineContextValue = { root?: RefObject; reduce: boolean; }; const TimelineContext = createContext({ reduce: false }); export interface TimelineProps { children: ReactNode; /** * Scroll container for contained scroll areas. Defaults to the viewport. * Drives both the line progress and each item's reveal. */ scrollContainer?: RefObject; className?: string; } // Dot pop: a touch of overshoot so the marker lands with weight. const DOT_POP: Transition = { type: "spring", stiffness: 520, damping: 22, mass: 0.6 }; const CONTENT_IN: Transition = { duration: 0.45, ease: EASE_OUT }; const PULSE: Transition = { duration: 1.6, ease: EASE_OUT, repeat: Number.POSITIVE_INFINITY }; export function Timeline({ children, scrollContainer, className }: TimelineProps) { const reduce = Boolean(useReducedMotion()); const listRef = useRef(null); const { scrollYProgress } = useScroll({ target: listRef, container: scrollContainer, // Line starts drawing as the list's top crosses 80% of the viewport and // completes when its end reaches the bottom edge. offset: ["start 0.8", "end end"], }); const progress = useSpring(scrollYProgress, SPRING_GLIDE); return (
    {children}
); } export interface TimelineItemProps { title: ReactNode; date?: ReactNode; /** ISO date for the `