southernmonsarysupply/components/process-timeline.tsx

134 lines
3.7 KiB
TypeScript

"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<HTMLDivElement | null>(null);
const stepRefs = useRef<Array<HTMLDivElement | null>>([]);
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 (
<div
ref={sectionRef}
className="process-timeline"
style={
{
"--timeline-progress": `${progress}`,
"--timeline-step-count": `${steps.length}`,
} as CSSProperties
}
>
<div className="process-rail" aria-hidden="true">
<div className="process-rail-track" />
<div className="process-rail-fill" />
<div className="process-rail-markers">
{markers.map((marker, index) => (
<span
key={marker}
className={`process-rail-marker ${
index <= activeIndex ? "is-active" : ""
}`}
/>
))}
</div>
</div>
<div className="process-rows">
{steps.map((step, index) => {
const sideClass = index % 2 === 0 ? "is-right" : "is-left";
const isActive = index <= activeIndex;
return (
<div
key={step.step}
ref={(node) => {
stepRefs.current[index] = node;
}}
className={`process-row ${sideClass} ${
isActive ? "is-active" : ""
}`}
>
<div className="process-row-spacer" aria-hidden="true" />
<div className="process-row-pin" aria-hidden="true" />
<article className="process-step-card">
<span className="process-step-number">{step.step}</span>
<h3>{step.title}</h3>
<p>{step.description}</p>
</article>
</div>
);
})}
</div>
</div>
);
}