"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 MEANINGFUL, recognizable icon paths for the morph (100x100 canvas). Drawn as filled silhouettes with similar complexity so MorphSVG tweens cleanly between them — never to nonsense: Audit -> magnifying glass Plan -> target (concentric rings + center) Execute -> rocket (rising) Report -> line chart trending up with plotted nodes */ const SHAPES = [ // Audit — magnifying glass (lens ring + handle) "M46 16a26 26 0 1 0 16 46.6l16.7 16.7a5 5 0 0 0 7-7L69 71.6A26 26 0 0 0 46 16Zm0 12a14 14 0 1 1 0 28 14 14 0 0 1 0-28Z", // Plan — target / bullseye (outer ring + mid ring + center dot) "M50 14a36 36 0 1 0 0 72 36 36 0 0 0 0-72Zm0 12a24 24 0 1 1 0 48 24 24 0 0 1 0-48Zm0 12a12 12 0 1 0 0 24 12 12 0 0 0 0-24Z", // Execute — rocket (nose, body, fins) rising "M50 12c12 8 18 22 18 38l-6 14H38l-6-14c0-16 6-30 18-38Zm0 20a6 6 0 1 0 0 12 6 6 0 0 0 0-12ZM34 66l-8 16 14-6Zm32 0 8 16-14-6Z", // Report — line chart trending up (axes + plotted line + nodes) "M22 18v54a6 6 0 0 0 6 6h54v-10H30V18ZM40 60l12-14 10 8 18-22 7 6-23 28-10-8-9 11Z", ]; 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"); const morphGlow = el.querySelector(".loop-glyph__glow"); 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 (+ its glow clone in sync) + a gentle counter-rotation // so it tumbles as it transforms, and grow the rail fill. tl.to(morph, { morphSVG: shape, duration: 1, ease: "power2.inOut" }, at); if (morphGlow) tl.to(morphGlow, { morphSVG: shape, duration: 1, ease: "power2.inOut" }, at); tl.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) */}
); }