/* global React, ReactDOM, gsap, ScrollTrigger, MotionPathPlugin */ /* eslint-disable */ const { useState, useEffect, useMemo, useRef, useCallback } = React; /* ============================================================ GSAP plugin registration ============================================================ */ if (typeof gsap !== "undefined") { gsap.registerPlugin(ScrollTrigger); if (typeof MotionPathPlugin !== "undefined") gsap.registerPlugin(MotionPathPlugin); } /* ============================================================ Storage helpers ============================================================ */ // ─── Cole aqui a URL do seu Google Apps Script após criar o webhook ─────────── const SHEETS_WEBHOOK_URL = "https://script.google.com/macros/s/AKfycbzIAjhNhyIZ2Cm17wcraZkfHMbhC-j1CCbpAE-RjW32G0CjRaJDBrpGzUxUml4gyhQ4EQ/exec"; // ───────────────────────────────────────────────────────────────────────────── const STORAGE_KEY = "mariany_rsvps_v1"; function loadRsvps() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); } catch { return []; } } function saveRsvp(r) { const entry = { ...r, id: Date.now(), createdAt: new Date().toISOString() }; // Salva localmente const all = loadRsvps(); all.unshift(entry); localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); // Envia para o Google Sheets (se configurado) if (SHEETS_WEBHOOK_URL) { fetch(SHEETS_WEBHOOK_URL, { method: "POST", mode: "no-cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entry), }).catch(() => {}); } } async function fetchRsvpsFromSheets() { if (!SHEETS_WEBHOOK_URL) return null; try { const res = await fetch(SHEETS_WEBHOOK_URL + "?action=list"); if (!res.ok) return null; return await res.json(); } catch { return null; } } /* ============================================================ Tweaks defaults ============================================================ */ const TWEAK_DEFAULTS = { palette: "blush", showFlyingButterflies: true, showParticles: true, particleDensity: 28, videoLabel: "Mariany", }; const PALETTES = { blush: { name: "Blush Garden", "--blush-50": "oklch(0.985 0.008 10)", "--blush-100": "oklch(0.965 0.018 12)", "--blush-200": "oklch(0.93 0.04 10)", "--blush-300": "oklch(0.88 0.07 10)", "--blush-400": "oklch(0.82 0.11 10)", "--candy-500": "oklch(0.74 0.16 12)", "--rose-600": "oklch(0.62 0.18 14)", "--rose-700": "oklch(0.48 0.16 16)", "--rose-900": "oklch(0.28 0.10 18)", "--lilac-200": "oklch(0.92 0.035 320)", }, candy: { name: "Candy Bloom", "--blush-50": "oklch(0.98 0.012 350)", "--blush-100": "oklch(0.96 0.026 350)", "--blush-200": "oklch(0.92 0.05 350)", "--blush-300": "oklch(0.86 0.09 350)", "--blush-400": "oklch(0.78 0.14 350)", "--candy-500": "oklch(0.7 0.2 350)", "--rose-600": "oklch(0.58 0.22 0)", "--rose-700": "oklch(0.44 0.18 5)", "--rose-900": "oklch(0.26 0.12 8)", "--lilac-200": "oklch(0.9 0.05 310)", }, pearl: { name: "Pearl Petal", "--blush-50": "oklch(0.99 0.004 20)", "--blush-100": "oklch(0.975 0.012 20)", "--blush-200": "oklch(0.95 0.025 20)", "--blush-300": "oklch(0.91 0.05 20)", "--blush-400": "oklch(0.86 0.08 18)", "--candy-500": "oklch(0.78 0.12 15)", "--rose-600": "oklch(0.66 0.14 18)", "--rose-700": "oklch(0.52 0.12 20)", "--rose-900": "oklch(0.32 0.08 22)", "--lilac-200": "oklch(0.94 0.025 320)", }, }; function applyPalette(key) { const p = PALETTES[key] || PALETTES.blush; const root = document.documentElement; Object.entries(p).forEach(([k, v]) => { if (k.startsWith("--")) root.style.setProperty(k, v); }); } /* ============================================================ GSAP reveal hook — replaces IntersectionObserver ============================================================ */ function useGsapReveal(active) { useEffect(() => { if (!active || typeof gsap === "undefined") return; // Immediately reveal elements in view const revealEls = document.querySelectorAll(".reveal:not(.in)"); revealEls.forEach((el) => { ScrollTrigger.create({ trigger: el, start: "top 88%", onEnter: () => { gsap.to(el, { opacity: 1, y: 0, duration: 1.1, ease: "power3.out", overwrite: "auto", }); el.classList.add("in"); }, once: true, }); // Reveal immediately if already in viewport const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight * 0.95) { gsap.to(el, { opacity: 1, y: 0, duration: 1.1, ease: "power3.out", overwrite: "auto" }); el.classList.add("in"); } }); }, [active]); } /* ============================================================ GSAP scene entrance hook ============================================================ */ function useGsapScenes(active) { const [activeScene, setActiveScene] = useState("hero"); useEffect(() => { if (!active || typeof gsap === "undefined") return; const scenes = document.querySelectorAll("section[data-scene]"); if (!scenes.length) return; const triggers = []; scenes.forEach((scene) => { const inner = scene.querySelector(".scene-inner"); if (!inner) return; // Entrance const enterTrigger = ScrollTrigger.create({ trigger: scene, start: "top 65%", onEnter: () => { gsap.to(inner, { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", duration: 1.4, ease: "power3.out", }); scene.classList.add("in"); scene.classList.remove("past"); setActiveScene(scene.dataset.scene); // Stagger child reveals const children = scene.querySelectorAll(".reveal:not(.in)"); gsap.to(children, { opacity: 1, y: 0, duration: 1.1, ease: "power3.out", stagger: 0.12, delay: 0.2, onStart() { children.forEach((c) => c.classList.add("in")); }, }); }, once: false, }); // Exit upward const exitTrigger = ScrollTrigger.create({ trigger: scene, start: "bottom 20%", onLeave: () => { gsap.to(inner, { opacity: 0.15, y: -30, scale: 0.97, filter: "blur(6px)", duration: 0.8, ease: "power2.in", }); scene.classList.add("past"); }, onEnterBack: () => { gsap.to(inner, { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", duration: 1.0, ease: "power3.out", }); scene.classList.remove("past"); setActiveScene(scene.dataset.scene); }, }); triggers.push(enterTrigger, exitTrigger); }); // Always show first scene const first = scenes[0]; if (first) { const fi = first.querySelector(".scene-inner"); if (fi) { gsap.set(fi, { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }); first.classList.add("in"); first.querySelectorAll(".reveal").forEach((el) => { gsap.to(el, { opacity: 1, y: 0, duration: 1.1, ease: "power3.out", stagger: 0.1 }); el.classList.add("in"); }); } } return () => { triggers.forEach((t) => t.kill()); }; }, [active]); return activeScene; } /* ============================================================ Topbar ============================================================ */ function Topbar({ route, go }) { const ref = useRef(null); useEffect(() => { if (!ref.current || typeof gsap === "undefined") return; gsap.fromTo(ref.current, { y: -60, opacity: 0 }, { y: 0, opacity: 1, duration: 1.2, ease: "power3.out", delay: 0.3 } ); }, []); return ( ); } /* ============================================================ Audio toggle ============================================================ */ function AudioToggle() { const [on, setOn] = useState(false); const audioRef = useRef(null); useEffect(() => { const audio = new Audio("/musica.mp3"); audio.loop = true; audio.volume = 0.45; audioRef.current = audio; // Attempt autoplay immediately; browsers may block it until user gesture audio.play().then(() => setOn(true)).catch(() => { // Fallback: start on first user interaction const unlock = () => { audio.play().then(() => setOn(true)).catch(() => {}); window.removeEventListener("click", unlock); window.removeEventListener("touchstart", unlock); window.removeEventListener("keydown", unlock); }; window.addEventListener("click", unlock, { once: true }); window.addEventListener("touchstart", unlock, { once: true }); window.addEventListener("keydown", unlock, { once: true }); }); return () => { audio.pause(); audio.src = ""; }; }, []); const toggle = () => { const audio = audioRef.current; if (!audio) return; if (!on) { audio.play().then(() => setOn(true)).catch(() => {}); } else { audio.pause(); setOn(false); } }; return ( ); } /* ============================================================ Hero — foto + GSAP cinematic entrance (sem timer) ============================================================ */ function Hero({ tweaks }) { const stageRef = useRef(null); // Cinematic entrance timeline useEffect(() => { if (!stageRef.current || typeof gsap === "undefined") return; const stage = stageRef.current; const tl = gsap.timeline({ delay: 0.4 }); // Photo wrap scales in from center tl.fromTo(stage.querySelector(".video-wrap"), { scale: 0.75, opacity: 0, y: 40 }, { scale: 1, opacity: 1, y: 0, duration: 1.6, ease: "back.out(1.3)" } ); // Left titles stagger in tl.fromTo( stage.querySelectorAll(".hero-side.left .hero-title, .hero-side.left .hero-lede, .hero-side.left .hero-meta"), { x: -40, opacity: 0 }, { x: 0, opacity: 1, duration: 1.2, ease: "power3.out", stagger: 0.15 }, "-=1.2" ); // Right titles stagger in tl.fromTo( stage.querySelectorAll(".hero-side.right .hero-title, .hero-side.right .hero-lede, .hero-side.right .hero-meta"), { x: 40, opacity: 0 }, { x: 0, opacity: 1, duration: 1.2, ease: "power3.out", stagger: 0.15 }, "-=1.2" ); }, []); // Subtle continuous parallax on photo-wrap useEffect(() => { if (!stageRef.current || typeof gsap === "undefined") return; const vw = stageRef.current.querySelector(".video-wrap"); if (!vw) return; const onMove = (e) => { const mx = (e.clientX / window.innerWidth - 0.5) * 14; const my = (e.clientY / window.innerHeight - 0.5) * 10; gsap.to(vw, { x: mx, y: my, duration: 1.4, ease: "power1.out", overwrite: "auto" }); }; window.addEventListener("mousemove", onMove); return () => window.removeEventListener("mousemove", onMove); }, []); return (
{tweaks.showFlyingButterflies && }
{/* Left — desktop subtitle, hidden on mobile */}

Há 1 ano…
o nosso mundo floresceu ✿

Da forma mais linda que poderíamos imaginar. Hoje, entre flores e borboletas, celebramos o primeiro capítulo do nosso conto encantado.

{/* Center — mobile label + photo */}
Convite especial
Mariany
{/* Right — desktop subtitle, hidden on mobile */}

Mariany nossa princesa

Está completando seu primeiro aninho e queremos viver esse momento mágico ao seu lado.

13 · 06 · 2026 16h00 Chácara 3 Irmãos
desça suavemente
); } /* ============================================================ Photo Carousel — 12 fotos com GSAP ============================================================ */ const CAROUSEL_PHOTOS = Array.from({ length: 12 }, (_, i) => `/foto${String(i + 1).padStart(2, "0")}.jpg`); function PhotoCarousel() { const [current, setCurrent] = useState(0); const trackRef = useRef(null); const autoRef = useRef(null); const goTo = useCallback((idx) => { const total = CAROUSEL_PHOTOS.length; const next = (idx + total) % total; setCurrent(next); if (trackRef.current) { gsap.to(trackRef.current, { x: `-${next * 100}%`, duration: 0.65, ease: "power2.inOut", }); } }, []); // Auto-advance every 3.5 s useEffect(() => { autoRef.current = setInterval(() => goTo(current + 1), 3500); return () => clearInterval(autoRef.current); }, [current, goTo]); const prev = () => { clearInterval(autoRef.current); goTo(current - 1); }; const next = () => { clearInterval(autoRef.current); goTo(current + 1); }; return (
{CAROUSEL_PHOTOS.map((src, i) => (
{`Foto
))}
{CAROUSEL_PHOTOS.map((_, i) => (
); } /* ============================================================ Garden (scene 2) — agora com copy do Conto + carrossel ============================================================ */ function GardenSection({ tweaks }) { return (
{/* ── Atmosphere: direct children of section (z-index 1) — never overlaps text ── */} {tweaks.showParticles && } {/* Flowers — only extreme corners */} {/* Butterflies — 10 spread along left + right edges only */} {tweaks.showFlyingButterflies && } {/* ── Text: inside scene-inner (z-index 2) — always above decorative layer ── */}

Cada sorriso da nossa princesa transformou nossos dias em um verdadeiro conto encantado.

Agora queremos celebrar esse capítulo especial ♡ ao lado das pessoas que amamos.

{/* Carrossel de fotos */}
); } /* ============================================================ Event info (scene 4) ============================================================ */ function EventSection({ onRSVP }) { return (
Os detalhes

Você está convidado para um dia mágico

Data
13 de Junho
Sábado, 2026
Horário
16:00
Recepção a partir das 15h30
Local
Chácara 3 Irmãos
Barra do Farias
Ver localização
); } /* ============================================================ Final (scene 5) ============================================================ */ function FinalSection({ onRSVP }) { return (

Seu carinho faz parte da nossa história…

Esperamos por você ✿

Para viver esse dia mágico ao nosso lado.

); } /* ============================================================ RSVP Modal ============================================================ */ function RSVPModal({ open, onClose }) { const [name, setName] = useState(""); const [phone, setPhone] = useState(""); const [count, setCount] = useState(0); const [companions, setCompanions] = useState([]); const [sent, setSent] = useState(false); useEffect(() => { if (open) { setName(""); setPhone(""); setCount(0); setCompanions([]); setSent(false); document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open]); useEffect(() => { setCompanions((prev) => { const next = [...prev]; while (next.length < count) next.push(""); next.length = count; return next; }); }, [count]); if (!open) return null; const submit = (e) => { e.preventDefault(); if (!name.trim() || !phone.trim()) return; saveRsvp({ name: name.trim(), phone: phone.trim(), count, companions: companions.map((c) => c.trim()).filter(Boolean), pix: 0 }); setSent(true); }; return (
e.stopPropagation()} role="dialog" aria-modal="true"> {!sent ? (

Confirmar presença

Será uma alegria ter você conosco ✿

setName(e.target.value)} placeholder="Como devemos te chamar?" required />
setPhone(e.target.value)} placeholder="(00) 00000-0000" required />
{count > 0 && (
Nome dos acompanhantes
{companions.map((c, i) => ( { const arr = [...companions]; arr[i] = e.target.value; setCompanions(arr); }} placeholder={`Acompanhante ${i + 1}`} /> ))}
)}
) : (
🦋

Presença confirmada!

Sua presença é o nosso maior presente.
Te esperamos no jardim encantado em 13 · 06 · 2026.

)}
); } /* ============================================================ Presentes page ============================================================ */ function PresentesPage({ tweaks }) { const [amount, setAmount] = useState(100); const [copied, setCopied] = useState(false); const pixKey = "08576056402"; const copy = () => { try { navigator.clipboard?.writeText(pixKey); } catch {} setCopied(true); setTimeout(() => setCopied(false), 1900); }; return (
{copied &&
Chave PIX copiada ✿
} {tweaks.showParticles && } {tweaks.showFlyingButterflies && }
Lista de presentes

O maior presente é compartilhar esse momento com você ♡

Mas, para quem desejar presentear nossa princesa, deixamos algumas sugestões com muito carinho — escolha o que tocar o seu coração.

01

Roupinhas

Vestidos, conjuntos e peças tamanho 2 a 3 anos para acompanhar nossa borboleta.

02

Sapatinhos & sandálias

Para pisar leve no jardim — tamanho 19 ou 20.

03

Brinquedos interativos

Que estimulem o desenvolvimento infantil — descoberta, cores e sons.

… ou presenteie via PIX ✿

Para quem preferir, também disponibilizamos esta opção delicada.

R$ {amount} ,00
Valor mínimo · R$ 100,00
); } /* ============================================================ PIX EMV payload builder (Banco Central do Brasil spec) ============================================================ */ function buildPixPayload(key, merchantName, city, amount) { // Format: ID (2) + Length (2) + Value const f = (id, val) => { const v = String(val); return id + String(v.length).padStart(2, "0") + v; }; const merchantInfo = f("26", f("00", "br.gov.bcb.pix") + f("01", key) ); const additional = f("62", f("05", "***")); let payload = [ f("00", "01"), // Payload format indicator merchantInfo, // Merchant account info (PIX) f("52", "0000"), // Merchant category code f("53", "986"), // Currency BRL amount > 0 ? f("54", amount.toFixed(2)) : "", // Transaction amount f("58", "BR"), // Country code f("59", merchantName.slice(0, 25)), // Merchant name f("60", city.slice(0, 15)), // Merchant city additional, // Reference label "6304", // CRC placeholder ].join(""); // CRC-16/CCITT-FALSE (polynomial 0x1021, init 0xFFFF) let crc = 0xFFFF; for (let i = 0; i < payload.length; i++) { crc ^= payload.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { crc = crc & 0x8000 ? (crc << 1) ^ 0x1021 : crc << 1; crc &= 0xFFFF; } } return payload + crc.toString(16).toUpperCase().padStart(4, "0"); } /* ============================================================ QR Code real — usa qrcodejs + payload PIX válido ============================================================ */ function QRPlaceholder({ amount }) { const containerRef = useRef(null); const pixPayload = useMemo( () => buildPixPayload("08576056402", "Mariany Aniversario", "Brasil", amount), [amount] ); useEffect(() => { const el = containerRef.current; if (!el) return; el.innerHTML = ""; if (typeof QRCode !== "undefined") { new QRCode(el, { text: pixPayload, width: 172, height: 172, colorDark: "#48162a", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.M, }); // qrcodejs inserts a table + img; make both fill the container el.querySelectorAll("img, canvas, table").forEach((n) => { n.style.cssText = "width:100%;height:100%;display:block;"; }); } else { el.innerHTML = `

QR Code indisponível — copie a chave acima

`; } }, [pixPayload]); return (
); } /* ============================================================ Admin confirmações ============================================================ */ function ConfirmacoesPage() { const [auth, setAuth] = useState(() => sessionStorage.getItem("admin_ok") === "1"); const [pw, setPw] = useState(""); const [err, setErr] = useState(""); const [filter, setFilter] = useState("all"); const [rows, setRows] = useState(() => loadRsvps()); const [syncing, setSyncing] = useState(false); const [syncMsg, setSyncMsg] = useState(""); // Auto-atualiza localStorage a cada 2s useEffect(() => { const id = setInterval(() => setRows(loadRsvps()), 2000); return () => clearInterval(id); }, []); // Ao entrar autenticado, busca do Sheets automaticamente se configurado useEffect(() => { if (auth && SHEETS_WEBHOOK_URL) handleSyncSheets(); }, [auth]); const handleSyncSheets = async () => { if (!SHEETS_WEBHOOK_URL) { setSyncMsg("⚠️ URL do Sheets não configurada."); setTimeout(() => setSyncMsg(""), 3000); return; } setSyncing(true); setSyncMsg(""); const data = await fetchRsvpsFromSheets(); setSyncing(false); if (!data) { setSyncMsg("❌ Não foi possível conectar ao Sheets."); } else { // Mescla com localStorage evitando duplicatas por id const local = loadRsvps(); const localIds = new Set(local.map(r => String(r.id))); // Normaliza linhas vindas do Sheets const remote = data.map(r => ({ id: r["ID"] || r.id || Date.now(), createdAt: r["Data"] || r.createdAt || new Date().toISOString(), name: r["Nome"] || r.name || "", phone: r["WhatsApp"] || r.phone || "", count: Number(r["Acompanhantes"] || r.count || 0), companions: r["Nomes Acompanhantes"] ? String(r["Nomes Acompanhantes"]).split(",").map(s => s.trim()).filter(Boolean) : (r.companions || []), pix: Number(r["PIX"] || r.pix || 0), })).filter(r => !localIds.has(String(r.id))); const merged = [...remote, ...local].sort((a, b) => b.id - a.id); localStorage.setItem(STORAGE_KEY, JSON.stringify(merged)); setRows(merged); setSyncMsg(`✿ Sincronizado — ${merged.length} confirmação(ões)`); } setTimeout(() => setSyncMsg(""), 4000); }; if (!auth) { const tryLogin = (e) => { e.preventDefault(); if (pw === "mariany1106") { sessionStorage.setItem("admin_ok", "1"); setAuth(true); setErr(""); } else { setErr("Senha incorreta — tente novamente."); } }; return (

Acesso reservado

Apenas a família tem acesso a esta área ✿

setPw(e.target.value)} placeholder="••••••••" autoFocus />
{err &&
{err}
}
); } const totalGuests = rows.reduce((s, r) => s + 1 + (r.count || 0), 0); const totalPix = rows.reduce((s, r) => s + (r.pix || 0), 0); const filtered = filter === "alone" ? rows.filter((r) => (r.count || 0) === 0) : filter === "group" ? rows.filter((r) => (r.count || 0) > 0) : rows; const logout = () => { sessionStorage.removeItem("admin_ok"); setAuth(false); }; const seedDemo = () => { if (rows.length > 0) return; const demo = [ { name: "Vovó Cláudia", phone: "(11) 98742-0011", count: 2, companions: ["Vovô Antônio","Tia Bruna"], pix: 200 }, { name: "Tia Larissa", phone: "(11) 99123-4501", count: 1, companions: ["Lucas Mendes"], pix: 0 }, { name: "Madrinha Aline",phone: "(21) 98810-2233", count: 3, companions: ["Padrinho Felipe","Júlia (5)","Bento (2)"], pix: 300 }, { name: "Patrícia Sales", phone: "(11) 99988-1100", count: 0, companions: [], pix: 100 }, ]; const all = demo.map((r, i) => ({ ...r, id: Date.now() + i, createdAt: new Date(Date.now() - i * 3600e3).toISOString() })); localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); setRows(all); }; return (

Confirmações

Painel privado · convidados da Mariany ✿

{syncMsg && {syncMsg}} {rows.length === 0 && }
Confirmações
{rows.length}
famílias
Convidados
{totalGuests}
pessoas ao todo
Acompanhantes
{rows.reduce((s, r) => s + (r.count || 0), 0)}
além dos titulares
PIX recebido
R${totalPix}
em presentes

Lista de convidados

{filtered.length === 0 ? (

Nenhuma confirmação ainda

As respostas aparecerão aqui assim que chegarem ✿

) : ( {filtered.map((r) => ( ))}
Nome WhatsApp Qtd. Acompanhantes PIX Data
{r.name} {r.phone} {r.count || 0} {(r.companions || []).length === 0 ? : (r.companions || []).map((c, i) => {c}) } {r.pix ? `R$${r.pix}` : "—"} {new Date(r.createdAt).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}
)}

Dados armazenados localmente neste navegador

); } /* ============================================================ Scene rail nav ============================================================ */ function SceneRail({ activeScene, hidden, locked }) { const scenes = [ { id: "hero", label: "Início" }, { id: "garden", label: "Jardim" }, { id: "event", label: "Evento" }, { id: "final", label: "Convite" }, ]; const go = (id) => { if (locked) return; const el = document.querySelector(`section[data-scene="${id}"]`); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); }; return (
{scenes.map((s) => ( go(s.id)} role="button" aria-label={s.label}> ))}
); } /* ============================================================ App root ============================================================ */ function App() { const initialRoute = (() => { const h = (window.location.hash || "").replace("#", "").replace("/", ""); return h === "presentes" || h === "confirmacoes" ? h : "home"; })(); const [route, setRoute] = useState(initialRoute); const [modal, setModal] = useState(false); const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); useEffect(() => { applyPalette(tweaks.palette); }, [tweaks.palette]); useEffect(() => { const onHash = () => { const h = (window.location.hash || "").replace("#", "").replace("/", ""); setRoute(h === "presentes" || h === "confirmacoes" ? h : "home"); }; window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); const go = (r) => { window.location.hash = r === "home" ? "" : "/" + r; setRoute(r); window.scrollTo({ top: 0, behavior: "smooth" }); // Refresh ScrollTrigger after route change setTimeout(() => { if (typeof ScrollTrigger !== "undefined") ScrollTrigger.refresh(); }, 80); }; useEffect(() => { const root = document.documentElement; if (route === "home") { root.classList.add("cinematic"); root.classList.remove("no-snap"); } else { root.classList.remove("cinematic"); root.classList.remove("no-snap"); } }, [route]); // GSAP scene entrance (replaces IntersectionObserver) const activeScene = useGsapScenes(route === "home"); // GSAP reveal for non-scene pages useGsapReveal(route !== "home"); return (
{route === "home" &&
); } ReactDOM.createRoot(document.getElementById("root")).render();