agency-web/app/components/Process.tsx

81 lines
2.4 KiB
TypeScript

"use client";
/**
* Process — "The Feedback Loop". The intro column pins (sticky) while the four
* steps scroll past; each step lights up as it reaches the viewport center, and
* a serif count in the sticky column advances with it. This is the
* scrollytelling beat. Degrades to a plain stacked list under reduced-motion
* (all steps shown active, no pin behaviour) and on narrow screens.
*/
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { processSteps } from "../content";
gsap.registerPlugin(ScrollTrigger);
export default function Process() {
const root = useRef<HTMLElement>(null);
const [active, setActive] = useState(0);
useEffect(() => {
const el = root.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setActive(processSteps.length - 1);
return;
}
const ctx = gsap.context(() => {
gsap.utils.toArray<HTMLElement>(".pstep").forEach((step, i) => {
ScrollTrigger.create({
trigger: step,
start: "top 60%",
end: "bottom 60%",
onToggle: (self) => {
if (self.isActive) setActive(i);
},
});
});
}, el);
return () => ctx.revert();
}, []);
return (
<section id="process" className="process" ref={root} aria-label="How it works">
<div className="wrap process__inner">
<div className="process__sticky">
<p className="kicker">How it works</p>
<h2 className="section__title">
The <span className="serif-em">Feedback Loop.</span>
</h2>
<p
className="process__count"
aria-hidden="true"
key={active}
>
0{active + 1}
</p>
</div>
<ol className="process__steps">
{processSteps.map((p, i) => (
<li
className={`pstep${i <= active ? " is-active" : ""}`}
key={p.n}
aria-current={i === active ? "step" : undefined}
>
<div className="pstep__top">
<span className="pstep__n">{p.n}</span>
<h3 className="pstep__name">{p.name}</h3>
</div>
<p className="pstep__desc">{p.desc}</p>
</li>
))}
</ol>
</div>
</section>
);
}