/* 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 ============================================================ */ const STORAGE_KEY = "mariany_rsvps_v1"; function loadRsvps() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); } catch { return []; } } function saveRsvp(r) { const all = loadRsvps(); all.unshift({ ...r, id: Date.now(), createdAt: new Date().toISOString() }); localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); } /* ============================================================ 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; 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 — 10-second lock + GSAP cinematic entrance ============================================================ */ const HERO_LOCK_SECONDS = 10; function Hero({ tweaks, locked, secondsLeft }) { const stageRef = useRef(null); const progress = 1 - secondsLeft / HERO_LOCK_SECONDS; const R = 32; const C = 2 * Math.PI * R; const dashOffset = C * (1 - progress); // Cinematic entrance timeline useEffect(() => { if (!stageRef.current || typeof gsap === "undefined") return; const stage = stageRef.current; const tl = gsap.timeline({ delay: 0.5 }); // Video wrap scales in from center tl.fromTo(stage.querySelector(".video-wrap"), { scale: 0.7, 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 video-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 && }

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.

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
{secondsLeft > 0 ? <>{secondsLeft}seg : }
{locked ? "assista nossa princesa" : "agora desça suavemente"}
desça suavemente
); } /* ============================================================ Garden (scene 2) ============================================================ */ 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 ── */}
Capítulo Um

Entre flores encantadas e brilho no ar

Borboletas dançam ao vento e cada pétala carrega um pedacinho de fantasia. Preparamos uma celebração cheia de amor, magia e memórias inesquecíveis — como um jardim que floresce só uma vez por ano.

); } /* ============================================================ Emotional (scene 3) ============================================================ */ function EmotionalSection() { return (
{/* ── Atmosphere: direct children of section ── */} {/* ── Text: inside scene-inner (z-index 2) ── */}
Um capítulo encantado

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

✦   com amor, papai & mamãe   ✦

Agora queremos celebrar esse capítulo tão especial ao lado das pessoas que amamos.

); } /* ============================================================ 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 — também tamanho 2 a 3 anos.

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()); useEffect(() => { const id = setInterval(() => setRows(loadRsvps()), 1500); return () => clearInterval(id); }, []); 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 ✿

{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: "emotional", label: "Conto" }, { 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}> ))}
); } /* ============================================================ Hero lock hook ============================================================ */ function useHeroLock(active) { const [secondsLeft, setSecondsLeft] = useState(HERO_LOCK_SECONDS); const locked = secondsLeft > 0; useEffect(() => { if (!active || secondsLeft <= 0) return; const t = setTimeout(() => setSecondsLeft((s) => Math.max(0, s - 1)), 1000); return () => clearTimeout(t); }, [secondsLeft, active]); useEffect(() => { if (!active || !locked) { document.documentElement.style.overflow = ""; document.body.style.overflow = ""; return; } document.documentElement.style.overflow = "hidden"; document.body.style.overflow = "hidden"; window.scrollTo({ top: 0, behavior: "auto" }); const prevent = (e) => e.preventDefault(); const onKey = (e) => { const keys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End", " ", "Spacebar"]; if (keys.includes(e.key)) e.preventDefault(); }; window.addEventListener("wheel", prevent, { passive: false }); window.addEventListener("touchmove", prevent, { passive: false }); window.addEventListener("keydown", onKey); return () => { window.removeEventListener("wheel", prevent); window.removeEventListener("touchmove", prevent); window.removeEventListener("keydown", onKey); document.documentElement.style.overflow = ""; document.body.style.overflow = ""; }; }, [active, locked]); const reset = useCallback(() => setSecondsLeft(HERO_LOCK_SECONDS), []); return { secondsLeft, locked, reset }; } /* ============================================================ 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); }; const { secondsLeft, locked, reset } = useHeroLock(route === "home"); useEffect(() => { if (route === "home") reset(); }, [route]); useEffect(() => { const root = document.documentElement; if (route === "home" && !locked) { root.classList.add("cinematic"); root.classList.remove("no-snap"); } else { root.classList.remove("cinematic"); root.classList[locked ? "add" : "remove"]("no-snap"); } }, [route, locked]); // GSAP scene entrance (replaces IntersectionObserver) const activeScene = useGsapScenes(route === "home"); // GSAP reveal for non-scene pages useGsapReveal(route !== "home"); // Re-run ScrollTrigger after unlock useEffect(() => { if (!locked && typeof ScrollTrigger !== "undefined") { setTimeout(() => ScrollTrigger.refresh(), 100); } }, [locked]); return (
{route === "home" &&
); } ReactDOM.createRoot(document.getElementById("root")).render();