"use client"; /** * PROCESS — "The Feedback Loop" as pinned scroll storytelling. * - GSAP ScrollTrigger pins the panel while the user scrolls through 4 steps. * - The LEFT column is a dark "stage card" (matching the work-cards' visual * language) that floats over the cream paper. Inside it a single SVG glyph * MORPHS between 4 shapes (MorphSVGPlugin) AND a per-step VISUAL PANEL swaps: * Audit -> a "revenue leak" line chart * Plan -> a channel x objective mini-matrix * Execute -> rising animated bars * Report -> a metric card * This kills the empty cream void and gives the section weight. * - A progress rail + numbered steps light up in sync (scrub). * - prefers-reduced-motion: no pin, no morph; panels render statically (all * visible, no blur) and steps render as a legible static list. */ import { useEffect, useRef } from "react"; // Side-effect import registers ScrollTrigger + MorphSVGPlugin; gsap exposes both. import { gsap } from "./gsap"; import { processSteps } from "../content"; // Four target paths for the morph (drawn on a 100x100 canvas). const SHAPES = [ // Audit — magnifying glass "M44 20a24 24 0 1 0 15 43l18 18 7-7-18-18A24 24 0 0 0 44 20Zm0 10a14 14 0 1 1 0 28 14 14 0 0 1 0-28Z", // Plan — connected route / nodes "M22 30a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm40 40a8 8 0 1 1 16 0 8 8 0 0 1-16 0ZM30 38v8a16 16 0 0 0 16 16h8a16 16 0 0 1 16 16v-2 2h-8a26 26 0 0 1-26-26v-8Z", // Execute — lightning bolt "M55 14 26 58h20l-6 30 32-46H50l9-28Z", // Report — bar chart trending up "M22 78V52h12v26Zm22 0V36h12v42Zm22 0V20h12v58ZM20 30 40 22l16 8 26-14", ]; const ACCENTS = ["#3b82f6", "#8b5cf6", "#10b981", "#7c3aed"]; export default function ProcessLoop() { const root = useRef(null); useEffect(() => { const el = root.current; if (!el) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) return; const ctx = gsap.context(() => { const morph = el.querySelector(".loop-glyph__path"); if (!morph) return; const steps = gsap.utils.toArray(".loop-step"); const panels = gsap.utils.toArray(".loop-panel"); const total = SHAPES.length; const setActive = (idx: number) => { steps.forEach((s, j) => s.classList.toggle("is-active", j === idx)); panels.forEach((p, j) => p.classList.toggle("is-active", j === idx)); el.style.setProperty("--loop-accent", ACCENTS[idx] ?? ACCENTS[0]); }; // One pinned, scrubbed timeline drives the morph + rotation + rail fill. // Trimmed end length (was innerHeight*(total-0.2)) so the pin is shorter // and the section no longer amplifies the emptiness. const tl = gsap.timeline({ scrollTrigger: { trigger: ".loop-pin", start: "top top", end: () => `+=${window.innerHeight * (total - 1.85)}`, pin: true, scrub: 0.6, anticipatePin: 1, onUpdate: (self) => { const idx = Math.min( total - 1, Math.floor(self.progress * total + 0.0001) ); setActive(idx); }, }, }); setActive(0); const counter = el.querySelector(".loop__count-cur"); const node = el.querySelector(".loop-rail__node"); SHAPES.forEach((shape, i) => { if (i === 0) return; // start shape already in markup const at = i - 1; // morph the glyph + a counter-rotation so it tumbles as it transforms, // and grow the rail fill (+ a node that rides the fill edge). tl.to(morph, { morphSVG: shape, duration: 1, ease: "power2.inOut" }, at) .to(".loop-glyph", { rotate: i * 5, duration: 1, ease: "power2.inOut" }, at) .to( ".loop-rail__fill", { scaleY: i / (total - 1), duration: 1, ease: "none" }, at ) .to( node, { top: `${(i / (total - 1)) * 100}%`, duration: 1, ease: "none" }, at ); }); // separate light update so the counter is robust at every scroll position tl.eventCallback("onUpdate", () => { const idx = Math.min(total, Math.floor(tl.progress() * total) + 1); if (counter) counter.textContent = String(idx).padStart(2, "0"); }); }, el); return () => ctx.revert(); }, []); return (

The Feedback Loop

How it works

{/* dark stage card — floats the glyph + per-step visual over the cream paper (mirrors how the work-cards float over the dark) */}
); }