// components.jsx — shared bits for Visimatic // Exports to window: VM (namespace with constants + components) const { useState, useEffect, useRef, useCallback, useMemo } = React; function useMobile(bp = 768) { const [mobile, setMobile] = useState(window.innerWidth < bp); useEffect(() => { const check = () => setMobile(window.innerWidth < bp); window.addEventListener("resize", check, { passive: true }); return () => window.removeEventListener("resize", check); }, [bp]); return mobile; } /* ─── data ──────────────────────────────────────────────────────────── */ const R2 = "https://pub-e01667d961094e52be5860918f659f12.r2.dev"; const SHOWREEL = { full: `${R2}/Logo%20Animation%20Clean.mp4`, loop: `${R2}/short%20loop%20Logo%20Animation%20Clean%203.mp4` }; const PORTFOLIO = [ { id: "embark1", title: "Embark", client: "Embark", style: "Kinetic", year: "2025", src: `${R2}/Embark%20(1).mp4`, aspect: "16/9" }, { id: "helvetic", title: "Helvetik", client: "Helvetik", style: "Minimal", year: "2025", src: `${R2}/HELVETIC.mp4`, aspect: "16/9" }, { id: "gokaido", title: "GOkaido", client: "GOkaido", style: "3D", year: "2024", src: `${R2}/GOkaido.mp4`, aspect: "16/9" }, { id: "ganymede", title: "Ganymede", client: "Ganymede Labs", style: "Minimal", year: "2025", src: `${R2}/Ganymede%20(1).mp4`, aspect: "16/9" }, { id: "grippi", title: "Grippi Media", client: "Grippi Media", style: "Minimal", year: "2024", src: `${R2}/Grippi%20Media%20%20(1).mp4`, aspect: "16/9" }, { id: "onpath", title: "On Path Studio", client: "On Path", style: "Kinetic", year: "2024", src: `${R2}/On%20Path%20Studio%202.mp4`, aspect: "16/9" }, { id: "embark2", title: "Embark — alt", client: "Embark", style: "Kinetic", year: "2024", src: `${R2}/Embark%20(2).mp4`, aspect: "16/9" }, { id: "ae", title: "AE Basics", client: "Personal", style: "Minimal", year: "2024", src: `${R2}/AE%20Basics.mp4`, aspect: "16/9" }]; const STYLES = [ { id: "minimal", label: "Minimal & clean", blurb: "Smooth, elegant, premium.", note: "best for SaaS, finance, premium brands" }, { id: "glitch", label: "Glitch / digital", blurb: "Tech, edgy, futuristic.", note: "best for AI, dev tools, web3" }, { id: "cartoon", label: "2D cartoon", blurb: "Playful, expressive, fun.", note: "best for kids, creators, mascots" }, { id: "unsure", label: "Leave it to you", blurb: "Trust the creative direction.", note: "I'll pick what fits your brand" }]; const PACKAGES = [ { id: "starter", name: "Starter", price: 150, delivery: "5 days", revisions: 1, popular: false, includes: ["Simple custom animation", "Custom colours & text", "MP4 export", "Commercial rights"] }, { id: "standard", name: "Standard", price: 225, delivery: "4 days", revisions: 1, popular: true, includes: ["Detailed animation", "Music + sound effects", "Logo transparency (PNG/MOV)", "Everything in Starter"] }, { id: "pro", name: "Pro", price: 300, delivery: "2 days", revisions: 2, popular: false, includes: ["Perfect loop", "Priority 2-day delivery", "2 revisions", "Everything in Standard"] }]; const ADDONS = [ { id: "transparency", label: "Logo transparency", price: 20, excludes: ["standard", "pro"] }, { id: "soundfx", label: "Sound effects", price: 25, excludes: ["standard", "pro"] }, { id: "loop", label: "Perfect loop", price: 30, excludes: ["pro"] }, { id: "revision", label: "Extra revision", price: 20, excludes: [] }, { id: "rush", label: "Rush delivery", price: 50, excludes: [] }, { id: "vectorize", label: "Logo vectorisation", price: 30, excludes: [] }, { id: "source", label: "Source file (AE)", price: 50, excludes: [] }]; const REVIEWS = [ { id: 1, name: "Maya C", role: "Founder", rating: 5, text: "Came back with a reveal that made our investors stop and rewind. Worth ten times what we paid.", delivery: "3 days" }, { id: 2, name: "Tomás R", role: "Marketing", rating: 5, text: "The first cut was the cut. Felt like he'd been inside the brand for a year.", delivery: "4 days" }, { id: 3, name: "Priya A", role: "Creator", rating: 5, text: "I have this on the top of every video now. People DM me about the logo, not the content.", delivery: "2 days" }, { id: 4, name: "Jules H", role: "Studio lead", rating: 5, text: "Got our brand mark moving in a way our in-house team had been chasing for months.", delivery: "3 days" }]; /* ─── prism (logo) ──────────────────────────────────────────────────── */ function Prism({ size = 28, animate = false, style = {} }) { return ( Visimatic prism); } /* ─── nav ───────────────────────────────────────────────────────────── */ function Nav({ route, setRoute }) { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); const mobile = useMobile(); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 16); onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); useEffect(() => { setOpen(false); }, [route]); const links = [ { id: "work", label: "Work" }, { id: "pricing", label: "Pricing" }, { id: "about", label: "About" }, { id: "contact", label: "Contact" }, ]; const logoBg = scrolled ? "rgba(8,8,16,0.82)" : "rgba(8,8,16,0.52)"; if (mobile) { return ( ); } return ( ); } /* ─── footer ────────────────────────────────────────────────────────── */ function Footer({ setRoute }) { const mobile = useMobile(); return ( ); } function FooterCol({ title, links }) { return (
{title}
{links.map((l, i) => )}
); } /* ─── video tile (auto-loop preview, hover scrub) ──────────────────── */ function VideoTile({ item, aspect = "16/9", fill = false, onClick, big = false }) { const ref = useRef(null); const [hover, setHover] = useState(false); useEffect(() => { const v = ref.current;if (!v) return; v.muted = true;v.loop = true;v.playsInline = true; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) v.play().catch(() => {});else v.pause(); }, { threshold: 0.15 }); io.observe(v); return () => io.disconnect(); }, []); return (
setHover(true)} onMouseLeave={() => setHover(false)} onClick={onClick} style={{ position: "relative", ...(fill ? { height: "100%" } : { aspectRatio: aspect }), borderRadius: big ? "var(--radius-lg)" : "var(--radius)", overflow: "hidden", border: "1px solid var(--line)", background: "var(--void-2)", cursor: onClick ? "pointer" : "default", transition: "transform 360ms cubic-bezier(.2,.7,.2,1), border-color 240ms", transform: hover ? "translateY(-3px)" : "translateY(0)", borderColor: hover ? "var(--line-2)" : "var(--line)" }}>