// 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 (
);
}
// 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 (
);
}
// 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,
});