// ui.jsx — shared primitives and atoms used across pages. // Globals: dj (data), React hooks. const { useState, useEffect, useRef, useCallback, useMemo } = React; const dj = window.DJ_DATA; // ───────────────────────────────────────────────────────────────────────── // Routing — hash-based // ───────────────────────────────────────────────────────────────────────── function useRoute() { const get = () => { const h = window.location.hash.replace(/^#\/?/, "") || "home"; const [path, ...rest] = h.split("/"); return { path, params: rest }; }; const [route, setRoute] = useState(get); useEffect(() => { const onHash = () => { setRoute(get()); // scroll to top on route change window.scrollTo({ top: 0, behavior: "instant" }); }; window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); return route; } function navigate(to) { window.location.hash = "#/" + to; } function Link({ to, children, className, style, onClick, ...rest }) { const handle = (e) => { e.preventDefault(); if (onClick) onClick(e); navigate(to); }; return ( {children} ); } // ───────────────────────────────────────────────────────────────────────── // In-view animation // ───────────────────────────────────────────────────────────────────────── function useInView(opts = {}) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; // Self-heal: if the element is already on-screen at mount (very common for // above-the-fold content), reveal on next frame. Deferring via rAF guarantees // a paint of the opacity:0 / translateY initial state BEFORE we flip to seen, // so the CSS transition has two distinct values to animate between. const reveal = () => { requestAnimationFrame(() => requestAnimationFrame(() => setSeen(true))); }; const inView = () => { const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; return r.top < vh && r.bottom > 0; }; if (inView()) { reveal(); return; } if (typeof IntersectionObserver === "undefined") { reveal(); return; } const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { reveal(); io.unobserve(el); } }); }, { threshold: 0.08, rootMargin: "0px 0px -6% 0px", ...opts } ); io.observe(el); // Belt-and-suspenders: re-check after a tick in case IO doesn't fire const t = setTimeout(() => { if (inView()) reveal(); }, 200); return () => { io.disconnect(); clearTimeout(t); }; }, []); return [ref, seen]; } function Reveal({ children, delay = 0, y = 24, as: As = "div", className, style, intensity = 1 }) { const [ref, seen] = useInView(); const dy = y * intensity; return ( {children} ); } // Stagger children — each direct child becomes a Reveal with sequenced delay function Stagger({ children, gap = 80, base = 0, ...rest }) { const arr = React.Children.toArray(children); return ( {arr.map((c, i) => ( {c} ))} ); } // ───────────────────────────────────────────────────────────────────────── // Buttons // ───────────────────────────────────────────────────────────────────────── function Button({ to, href, children, kind = "primary", size = "md", icon, onClick, className = "" }) { const cls = `dj-btn dj-btn-${kind} dj-btn-${size} ${className}`; const inner = ( {children} {icon !== false && ( )} ); if (to) return {inner}; if (href) return {inner}; return ; } // ───────────────────────────────────────────────────────────────────────── // Eyebrow / section header // ───────────────────────────────────────────────────────────────────────── function Eyebrow({ children, mark = true, style }) { return (
{mark && } {children}
); } function SectionHead({ eyebrow, title, intro, align = "left", id }) { return (
{eyebrow && {eyebrow}}

{title}

{intro &&

{intro}

}
); } // ───────────────────────────────────────────────────────────────────────── // Counter // ───────────────────────────────────────────────────────────────────────── function Counter({ value, suffix = "", prefix = "", decimals = 0, duration = 1400 }) { const [ref, seen] = useInView(); const [n, setN] = useState(0); useEffect(() => { if (!seen) return; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setN(value * eased); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, [seen, value, duration]); return ( {prefix} {decimals ? n.toFixed(decimals) : Math.round(n).toLocaleString()} {suffix} ); } // ───────────────────────────────────────────────────────────────────────── // Marquee — infinite logo strip // ───────────────────────────────────────────────────────────────────────── function Marquee({ items, speed = 40, className = "" }) { const doubled = [...items, ...items]; return (
{doubled.map((it, i) => ( {it} ))}
); } // ───────────────────────────────────────────────────────────────────────── // Logo mark — wordmark for Digital Jaao // ───────────────────────────────────────────────────────────────────────── function Logo({ size = 18, color }) { return ( Digital Jaao ); } // ───────────────────────────────────────────────────────────────────────── // Niche chip / filter // ───────────────────────────────────────────────────────────────────────── function Chip({ active, onClick, children }) { return ( ); } // Placeholder visual block — for hero media, reels, client images function MediaSlot({ label, ratio = "4 / 5", color = "var(--ink)", className = "", children }) { return (
{children} {label}
); } // Icon system — simple line SVGs keyed by name function Icon({ name, size = 22 }) { const s = size; const p = { stroke: "currentColor", strokeWidth: 1.5, fill: "none", strokeLinecap: "round", strokeLinejoin: "round", }; const paths = { agent: , voice: , spark: , atom: , chat: , loop: , flow: , node: , funnel: , stack: , meta: , google: , pin: , chart: , search: , share: , shape: , play: , }; return ( {paths[name] || paths.spark} ); } // Export to window for cross-file usage Object.assign(window, { useRoute, navigate, Link, useInView, Reveal, Stagger, Button, Eyebrow, SectionHead, Counter, Marquee, Logo, Chip, MediaSlot, Icon, dj, });