// Componentes globais compartilhados (nav, footer, grain, intro) // Carregar via const { useState, useEffect, useRef } = React; // WhatsApp oficial da MARA (link com mensagem pré-preenchida) const WHATSAPP = "https://wa.me/5562994647141?text=Oi!%20Estava%20no%20site%20de%20voc%C3%AAs%20e%20gostaria%20de%20conversar%20sobre%20a%20produ%C3%A7%C3%A3o%20de%20um%20projeto"; function WhatsAppIcon({ size = 14 }) { return ( ); } function Grain() { return ; } function Nav({ active }) { const [logoOk, setLogoOk] = useState(true); return ( <> {/* Botão flutuante de WhatsApp — fora do #menu (que usa mix-blend), em todas as páginas */} ); } function SplitLine({ text, className = "", delayOffset = 0 }) { // wrap each char in a span with .split-line for animation const chars = [...text]; return ( {chars.map((c, i) => {c === " " ? " " : c} )} ); } function PlayIcon({ size = 18 }) { return ( ); } function ArrowIcon() { return ( ); } // Generic cinematic placeholder / thumbnail cover for project videos function VideoStill({ label, timecode = "00:00:14:21", duration = "00:18", tone = 0, aspect = "16/10", thumbnail }) { const hueShift = [0, -8, 6, -14, 12, 0][tone % 6]; const baseStyle = { aspectRatio: aspect, filter: thumbnail ? "none" : `hue-rotate(${hueShift}deg)`, backgroundImage: thumbnail ? `url(${thumbnail}), linear-gradient(135deg, #3a3938 0%, #2a2928 50%, #1f1e1d 100%)` : null, backgroundSize: "cover", backgroundPosition: "center 20%" }; return (
{thumbnail &&
} {label &&
REC ● {label}
}
{timecode}
); } function SpinBadge({ text = "QUE MARA! • QUE MARA! • ", size = 140, color = "var(--color-dark)" }) { const radius = size / 2 - 12; // raio do anel de texto const uid = Math.random().toString(36).slice(2, 8); const id = "tp-" + uid; const maskId = "mk-" + uid; const maskRef = useRef(null); const coreRef = useRef(null); const cx = size / 2, cy = size / 2; const coreR = size * 0.20; // raio do ponto vermelho (= inset 30% do .core antigo) const circleD = `M ${cx} ${cy - coreR} A ${coreR} ${coreR} 0 1 1 ${cx} ${cy + coreR} A ${coreR} ${coreR} 0 1 1 ${cx} ${cy - coreR} Z`; // triângulo de "play" centralizado (centróide no centro), apontando para a direita const a = coreR * 1.1, b = coreR * 0.55, hh = coreR * 0.9; const playD = `M ${cx + a} ${cy} L ${cx - b} ${cy - hh} L ${cx - b} ${cy + hh} Z`; React.useLayoutEffect(() => { // no mobile o selo fica estático (sem DrawSVG/Morph) — menos processamento if (typeof gsap === "undefined" || window.innerWidth <= 768) return; const tweens = []; // DrawSVG numa máscara: as palavras "preenchem" ao redor do círculo ao abrir if (window.DrawSVGPlugin && maskRef.current) { gsap.registerPlugin(window.DrawSVGPlugin); tweens.push(gsap.fromTo(maskRef.current, { drawSVG: "0%" }, { drawSVG: "100%", duration: 1.6, ease: "power1.inOut", delay: 0.2 })); } // MorphSVG: o ponto vermelho alterna entre círculo e triângulo de play if (window.MorphSVGPlugin && coreRef.current) { gsap.registerPlugin(window.MorphSVGPlugin); tweens.push(gsap.to(coreRef.current, { morphSVG: playD, duration: 1.3, ease: "power2.inOut", repeat: -1, yoyo: true, repeatDelay: 1.0 })); } return () => tweens.forEach(t => t && t.kill()); }, []); return (
{/* camada que gira: só o anel de palavras */} {text.repeat(3)} {/* camada fixa: o ponto vermelho que vira play */}
); } function Footer() { const [tc, setTc] = useState("00:00:00:00"); useEffect(() => { // marca o início da sessão (persiste entre navegações até fechar a aba) let start = Number(sessionStorage.getItem("mara-session-start")); if (!start) { start = Date.now(); sessionStorage.setItem("mara-session-start", String(start)); } const tick = () => { const elapsed = Date.now() - start; const totalSec = Math.floor(elapsed / 1000); const hh = String(Math.floor(totalSec / 3600)).padStart(2, "0"); const mm = String(Math.floor((totalSec % 3600) / 60)).padStart(2, "0"); const ss = String(totalSec % 60).padStart(2, "0"); const ff = String(Math.floor((elapsed % 1000) / 1000 * 24)).padStart(2, "0"); // 24fps setTc(`${hh}:${mm}:${ss}:${ff}`); }; tick(); const id = setInterval(tick, 1000 / 12); // 12hz suficiente pro frame piscar return () => clearInterval(id); }, []); return ( ); } function IntroOverlay({ onDone }) { const [gone, setGone] = useState(false); useEffect(() => { // only on first visit per session if (sessionStorage.getItem("mara-intro-played")) { setGone(true); onDone && onDone(); return; } const t = setTimeout(() => { setGone(true); sessionStorage.setItem("mara-intro-played", "1"); onDone && onDone(); }, 2400); return () => clearTimeout(t); }, []); return (
MARA
); } // scroll-trigger fade-up for elements with .fade-up function useFadeUp() { useEffect(() => { const els = document.querySelectorAll(".fade-up"); const io = new IntersectionObserver((entries) => { entries.forEach((e) => {if (e.isIntersecting) e.target.classList.add("in");}); }, { threshold: 0.15 }); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }, []); } Object.assign(window, { Grain, Nav, Footer, IntroOverlay, SplitLine, PlayIcon, ArrowIcon, VideoStill, SpinBadge, useFadeUp });