"use client"; import type { CSSProperties } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import type { ProcessStep } from "@/data/site-content"; type ProcessTimelineProps = { steps: ProcessStep[]; }; function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } export function ProcessTimeline({ steps }: ProcessTimelineProps) { const sectionRef = useRef(null); const stepRefs = useRef>([]); const [activeIndex, setActiveIndex] = useState(0); const [progress, setProgress] = useState(0); const markers = useMemo(() => steps.map((step) => step.step), [steps]); useEffect(() => { function updateTimeline() { const section = sectionRef.current; if (!section) { return; } const sectionRect = section.getBoundingClientRect(); const viewportAnchor = window.innerHeight * 0.42; const rawProgress = (viewportAnchor - sectionRect.top) / Math.max(sectionRect.height - window.innerHeight * 0.3, 1); setProgress(clamp(rawProgress, 0, 1)); let closestIndex = 0; let closestDistance = Number.POSITIVE_INFINITY; stepRefs.current.forEach((stepNode, index) => { if (!stepNode) { return; } const rect = stepNode.getBoundingClientRect(); const center = rect.top + rect.height / 2; const distance = Math.abs(center - viewportAnchor); if (distance < closestDistance) { closestDistance = distance; closestIndex = index; } }); setActiveIndex(closestIndex); } updateTimeline(); let frame = 0; function onScrollOrResize() { cancelAnimationFrame(frame); frame = window.requestAnimationFrame(updateTimeline); } window.addEventListener("scroll", onScrollOrResize, { passive: true }); window.addEventListener("resize", onScrollOrResize); return () => { cancelAnimationFrame(frame); window.removeEventListener("scroll", onScrollOrResize); window.removeEventListener("resize", onScrollOrResize); }; }, []); return (