"use client"; import dynamic from "next/dynamic"; import { useEffect, useRef, useState } from "react"; import { buildWhatsAppUrl } from "@/lib/whatsapp"; const StoryStage3D = dynamic(() => import("./StoryStage3D"), { ssr: false }); type Chapter = { kicker: string; lines: string[]; body?: string; stats?: Array<{ value: number; suffix: string; label: string }>; cta?: boolean; }; /** * Reel scrollytelling da home: palco sticky de 100dvh dirigido por uma única * timeline GSAP com scrub — o scroll é o controle de play/rebobinar. * Sem JS ou com prefers-reduced-motion: capítulos renderizam empilhados, * inteiros e legíveis (mesma regra de fail-soft do hero). */ export default function ScrollStory({ projectsCount, experiencesCount, skillsCount, }: { projectsCount: number; experiencesCount: number; skillsCount: number; }) { const chapters: Chapter[] = [ { kicker: "01 — Visão geral", lines: ["Software que opera.", "Não só roda."], body: "Role para rebobinar a engenharia por trás de cada sistema — a mesma página, agora em tomada contínua.", }, { kicker: "02 — Arquitetura", lines: ["Da borda", "ao painel."], body: "Dispositivo, integração, API e interface girando juntos: contratos claros entre camada física e produto.", }, { kicker: "03 — Em produção", lines: ["Entrega com", "critério real."], stats: [ { value: projectsCount, suffix: "", label: "sistemas ativos no portfólio" }, { value: experiencesCount, suffix: "", label: "ciclos de produto entregues" }, { value: skillsCount, suffix: "", label: "tecnologias em operação" }, ], }, { kicker: "04 — Próximo ciclo", lines: ["Vamos girar o", "seu próximo sistema."], cta: true, }, ]; const sectionRef = useRef(null); const progressRef = useRef(0); const [engine, setEngine] = useState(false); const [webgl, setWebgl] = useState(false); useEffect(() => { const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduced) return undefined; setEngine(true); setWebgl( !window.matchMedia("(pointer: coarse)").matches && window.innerWidth >= 768, ); return undefined; }, []); useEffect(() => { if (!engine) return undefined; const el = sectionRef.current; if (!el) 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 scenes = gsap.utils.toArray("[data-story-scene]"); const bar = el.querySelector("[data-story-bar]"); const indexEl = el.querySelector("[data-story-index]"); const hint = el.querySelector("[data-story-hint]"); gsap.set(scenes.slice(1), { autoAlpha: 0 }); gsap.set(scenes, { yPercent: 0 }); const tl = gsap.timeline({ defaults: { ease: "none" }, scrollTrigger: { trigger: el, start: "top top", end: "bottom bottom", scrub: 0.85, onUpdate: (self) => { progressRef.current = self.progress; if (indexEl) { const i = Math.min( scenes.length, Math.floor(self.progress * scenes.length) + 1, ); indexEl.textContent = String(i).padStart(2, "0"); } }, }, }); const span = 1 / scenes.length; scenes.forEach((scene, i) => { const at = i * span; const inner = scene.querySelectorAll("[data-story-line]"); const kicker = scene.querySelector("[data-story-kicker]"); const body = scene.querySelectorAll("[data-story-body]"); const stats = scene.querySelectorAll("[data-story-stat]"); tl.fromTo( inner, { yPercent: 118 }, { yPercent: 0, duration: span * 0.34, stagger: span * 0.05 }, at + span * 0.02, ); if (kicker) tl.fromTo( kicker, { autoAlpha: 0, y: 10 }, { autoAlpha: 1, y: 0, duration: span * 0.16 }, at, ); if (body.length) tl.fromTo( body, { autoAlpha: 0, y: 22 }, { autoAlpha: 1, y: 0, duration: span * 0.22, stagger: span * 0.04 }, at + span * 0.18, ); if (stats.length) tl.fromTo( stats, { autoAlpha: 0, y: 26, filter: "blur(6px)" }, { autoAlpha: 1, y: 0, filter: "blur(0px)", duration: span * 0.24, stagger: span * 0.05, }, at + span * 0.2, ); // entrada da cena + saída — janelas sobrepostas p/ crossfade contínuo tl.fromTo( scene, { autoAlpha: 0 }, { autoAlpha: 1, duration: span * 0.18 }, Math.max(0, at - span * 0.08), ); if (i < scenes.length - 1) tl.to(scene, { autoAlpha: 0, yPercent: -5, duration: span * 0.18 }, at + span * 0.8); if (stats.length) { stats.forEach((statEl) => { const numEl = statEl.querySelector("[data-story-num]"); if (!numEl) return; const target = Number(numEl.dataset.storyNumTo ?? "0"); const counter = { v: 0 }; tl.to( counter, { v: target, duration: span * 0.3, ease: "power2.out", onUpdate: () => { numEl.textContent = String(Math.round(counter.v)); }, }, at + span * 0.22, ); }); } }); if (bar) tl.fromTo(bar, { scaleX: 0 }, { scaleX: 1, duration: 1 }, 0); if (hint) tl.to(hint, { autoAlpha: 0, duration: span * 0.1 }, span * 0.12); }, el); cleanup = () => ctx.revert(); }; void setup(); return () => { cancelled = true; cleanup(); }; }, [engine]); // ---- Fallback estático: capítulos empilhados, conteúdo inteiro ---- if (!engine) { return (
O reel
{chapters.map((ch) => (
{ch.kicker}

{ch.lines.join(" ")}

{ch.body && (

{ch.body}

)} {ch.stats && (
    {ch.stats.map((s) => (
  • {s.value} {s.suffix}
    {s.label}
  • ))}
)} {ch.cta && ( Conversar agora )}
))}
); } // ---- Modo reel: palco sticky com cenas em crossfade ---- return (
{/* fundo: gradiente radial + brilho do WebGL */}
{webgl && (
)} {/* legibilidade do texto sobre a cena */}
{/* cenas */}
{chapters.map((ch, i) => (
{ch.kicker}

{ch.lines.map((line) => ( {line} ))}

{ch.body && (

{ch.body}

)} {ch.stats && (
    {ch.stats.map((s) => (
  • 0 {s.suffix}
    {s.label}
  • ))}
)} {ch.cta && ( Conversar agora )}
))}
{/* HUD: índice de capítulo + barra de progresso do reel */}
01 / 0{chapters.length}
↕ scroll = play / rewind
); }