"use client"; import { useCallback, useLayoutEffect, useRef, useState } from "react"; import { milestones } from "@/components/roadmap/milestones"; import { TimelineItem } from "@/components/roadmap/timeline-item"; import { TimelineRail } from "@/components/roadmap/timeline-rail"; import { getMilestoneSide, } from "@/components/roadmap/timeline-config"; import { useActiveTimelineItem } from "@/components/roadmap/use-active-timeline-item"; import { Container } from "@/components/container"; import { MotionReveal } from "@/components/motion-reveal"; import { layout } from "@/lib/layout"; import { type } from "@/lib/typography"; import { cn } from "@/lib/utils"; type RoadmapSectionProps = { className?: string; }; /** Stacked timeline (nodes above cards) below Tailwind `lg`. */ const STACKED_RAIL_MQ = "(max-width: 1023px)"; export function RoadmapSection({ className }: RoadmapSectionProps) { const sectionRef = useRef(null); const trackRef = useRef(null); const nodeRefs = useRef<(HTMLDivElement | null)[]>([]); const cardRefs = useRef<(HTMLElement | null)[]>([]); const [railMetrics, setRailMetrics] = useState({ offsetTop: 0, trackHeight: 0, progressHeight: 0, }); const { activeIndex, setItemRef, selectActiveIndex } = useActiveTimelineItem( milestones.length, sectionRef ); const setNodeRef = useCallback( (index: number) => (element: HTMLDivElement | null) => { nodeRefs.current[index] = element; }, [] ); const setCardRef = useCallback( (index: number) => (element: HTMLElement | null) => { cardRefs.current[index] = element; }, [] ); const updateRailMetrics = useCallback(() => { const track = trackRef.current; const firstNode = nodeRefs.current[0]; const lastNode = nodeRefs.current[milestones.length - 1]; const activeNode = nodeRefs.current[activeIndex]; if (!track || !firstNode || !lastNode || !activeNode) return; const trackTop = track.getBoundingClientRect().top; const stacked = typeof window !== "undefined" && window.matchMedia(STACKED_RAIL_MQ).matches; if (stacked) { const firstCard = cardRefs.current[0]; const lastCard = cardRefs.current[milestones.length - 1]; const activeCard = cardRefs.current[activeIndex]; if (!firstCard || !lastCard || !activeCard) return; // Card centers keep the rail behind end cards — never above the first // or below the last (nodes sit above cards in the stacked layout). const firstRect = firstCard.getBoundingClientRect(); const lastRect = lastCard.getBoundingClientRect(); const activeRect = activeCard.getBoundingClientRect(); const railStart = firstRect.top + firstRect.height / 2 - trackTop; const railEnd = lastRect.top + lastRect.height / 2 - trackTop; const activeCenter = activeRect.top + activeRect.height / 2 - trackTop; setRailMetrics({ offsetTop: railStart, trackHeight: Math.max(0, railEnd - railStart), progressHeight: Math.max(0, activeCenter - railStart), }); return; } const firstCenter = firstNode.getBoundingClientRect().top + firstNode.offsetHeight / 2; const lastCenter = lastNode.getBoundingClientRect().top + lastNode.offsetHeight / 2; const activeCenter = activeNode.getBoundingClientRect().top + activeNode.offsetHeight / 2; setRailMetrics({ offsetTop: firstCenter - trackTop, trackHeight: Math.max(0, lastCenter - firstCenter), progressHeight: Math.max(0, activeCenter - firstCenter), }); }, [activeIndex]); useLayoutEffect(() => { updateRailMetrics(); window.addEventListener("resize", updateRailMetrics, { passive: true }); window.addEventListener("scroll", updateRailMetrics, { passive: true }); const media = window.matchMedia(STACKED_RAIL_MQ); const onMediaChange = () => updateRailMetrics(); media.addEventListener("change", onMediaChange); const track = trackRef.current; const observer = track && typeof ResizeObserver !== "undefined" ? new ResizeObserver(updateRailMetrics) : null; if (track) observer?.observe(track); return () => { window.removeEventListener("resize", updateRailMetrics); window.removeEventListener("scroll", updateRailMetrics); media.removeEventListener("change", onMediaChange); observer?.disconnect(); }; }, [updateRailMetrics]); return (

مسیر همکاری

نقشه راه امین رایان

هر مرحله با اسکرول یا کلیک فعال می‌شود

{/* dir=ltr keeps physical left/right card alternation consistent */}
    {milestones.map((milestone, index) => ( selectActiveIndex(index)} /> ))}
); }