"use client"; import { useEffect, useRef, useState } from "react"; /** * Palco 2 do estilo product-page: filmstrip horizontal dirigido pelo scroll. * Arquitetura: runway 280vh + palco `position: sticky` (mesma técnica provada * no reel do hero — sem GSAP pin, sem wrapper de flex). O scrub move a faixa * da direita para a esquerda; quando a faixa termina, o sticky solta e o * scroll continua para baixo normalmente. Role para cima = rebobina. * * Sem JS / reduced motion: grid estático (fallback), como no ScrollStory. */ const PRINTS = [ { src: "/assets/prints/print-nutriscan.png", name: "NutriScan", desc: "PWA de dados nutricionais sobre Open Food Facts — busca, Nutri-Score e NOVA.", url: "https://nutriscan.evandro.dev.br", accent: true, }, { src: "/assets/prints/print-scplus.png", name: "SCPLUS", desc: "Educação e tecnologia sustentável em Garuva-SC; interface institucional viva.", url: "https://scplus.evandro.dev.br", accent: false, }, { src: "/assets/prints/print-sev.png", name: "SEV Moda Íntima", desc: "E-commerce em produção com checkout e catálogo — Next.js ponta a ponta.", url: "https://sevmodaintima.com.br", accent: true, }, { src: "/assets/prints/print-asservicos.png", name: "AS Serviços", desc: "Presença digital SSR para operação de serviços, pronta para crescer.", url: "https://asservicos.com", accent: false, }, ]; const CARD_W = "min(80vw, 430px)"; export default function PrintReel() { const sectionRef = useRef(null); const [engine, setEngine] = useState(false); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return undefined; setEngine(true); return undefined; }, []); useEffect(() => { if (!engine) return undefined; const section = sectionRef.current; if (!section) return undefined; let cleanup = () => {}; let cancelled = false; const setup = async () => { const [{ gsap }, { ScrollTrigger }] = await Promise.all([ import("gsap"), import("gsap/ScrollTrigger"), ]); if (cancelled) return; gsap.registerPlugin(ScrollTrigger); const ctx = gsap.context(() => { const track = section.querySelector("[data-strip-track]"); const idxEl = section.querySelector("[data-strip-index]"); const bar = section.querySelector("[data-strip-bar]"); if (!track) return; // distância = largura da faixa menos a viewport const distance = () => Math.max(0, track.scrollWidth - window.innerWidth); const tl = gsap.timeline({ defaults: { ease: "none" }, scrollTrigger: { trigger: section, start: "top top", end: "bottom bottom", scrub: 0.85, invalidateOnRefresh: true, }, }); tl.to(track, { x: () => -distance(), duration: 1 }); if (bar) tl.fromTo(bar, { scaleX: 0 }, { scaleX: 1, duration: 1 }, 0); if (idxEl) { tl.eventCallback("onUpdate", () => { const p = (tl.scrollTrigger?.progress ?? 0) * PRINTS.length; idxEl.textContent = String( Math.min(PRINTS.length, Math.floor(p) + 1), ).padStart(2, "0"); }); } // cada card "acende" ao entrar e esmaece ao passar (scrub, reversível) const cards = Array.from( section.querySelectorAll("[data-strip-card]"), ); const span = 1 / cards.length; cards.forEach((card, i) => { tl.fromTo( card, { opacity: 0.15, scale: 0.92, rotateY: -9 }, { opacity: 1, scale: 1, rotateY: 0, transformPerspective: 1100, duration: span * 0.5, }, Math.max(0, i * span - span * 0.12), ); if (i > 0) { tl.to( card, { opacity: 0.35, scale: 0.94, duration: span * 0.4 }, (i - 1) * span + span * 0.55, ); } }); }, section); cleanup = () => ctx.revert(); }; void setup(); return () => { cancelled = true; cleanup(); }; }, [engine]); // ---- Fallback estático (sem JS / reduced motion) ---- if (!engine) { return ( ); } // ---- Modo reel: runway + sticky ---- return ( ); }