// ─── UI UTILITIES ────────────────────────────────────────────────────── // Cursor dot, AnimatedNumber, Reveal observer, useScroll helpers const { useEffect, useRef, useState, useMemo, useCallback } = React; // ─── CursorDot ───────────────────────────────────────────────────────── function CursorDot() { const ref = useRef(null); const [visible, setVisible] = useState(false); const [expand, setExpand] = useState(false); useEffect(() => { let raf = 0; let tx = 0, ty = 0, cx = 0, cy = 0; const onMove = (e) => { tx = e.clientX; ty = e.clientY; setVisible(true); }; const onLeave = () => setVisible(false); const interactiveSel = "a, button, .btn, .case-card, .problem-item, .roi-row, .platform-pill, .t-card, [data-magnetic]"; const onOver = (e) => { if (e.target.closest && e.target.closest(interactiveSel)) setExpand(true); }; const onOut = (e) => { if (e.target.closest && e.target.closest(interactiveSel)) setExpand(false); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseleave", onLeave); document.addEventListener("mouseover", onOver); document.addEventListener("mouseout", onOut); const tick = () => { cx += (tx - cx) * 0.22; cy += (ty - cy) * 0.22; if (ref.current) { ref.current.style.transform = `translate(${cx}px, ${cy}px) translate(-50%, -50%)`; } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseleave", onLeave); document.removeEventListener("mouseover", onOver); document.removeEventListener("mouseout", onOut); }; }, []); return React.createElement("div", { ref, className: `cursor-dot ${visible ? "show" : ""} ${expand ? "expand" : ""}`, "aria-hidden": "true", }); } // ─── Reveal on scroll ────────────────────────────────────────────────── function useReveal() { useEffect(() => { const els = document.querySelectorAll(".reveal"); // Fallback timer — if IntersectionObserver doesn't fire for any reason, // ensure content is visible after a short delay so the page never appears blank. const fallback = setTimeout(() => { els.forEach((el) => el.classList.add("in")); }, 600); let io; try { io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { rootMargin: "0px 0px -10% 0px", threshold: 0.01 } ); els.forEach((el) => io.observe(el)); } catch (err) { // No IntersectionObserver — reveal everything immediately. els.forEach((el) => el.classList.add("in")); } return () => { clearTimeout(fallback); if (io) io.disconnect(); }; }, []); } // ─── AnimatedNumber ──────────────────────────────────────────────────── function AnimatedNumber({ to, prefix = "", suffix = "", duration = 1400, decimals = 0 }) { const ref = useRef(null); const [val, setVal] = useState(0); const [started, setStarted] = useState(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !started) { setStarted(true); } }, { threshold: 0.4 } ); io.observe(ref.current); return () => io.disconnect(); }, [started]); useEffect(() => { if (!started) return; const t0 = performance.now(); let raf = 0; const tick = (t) => { const p = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - p, 4); setVal(to * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [started, to, duration]); const formatted = useMemo(() => { const n = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toString(); return n.replace(/\B(?=(\d{3})+(?!\d))/g, "."); }, [val, decimals]); return React.createElement("span", { ref, className: "tabular" }, prefix, formatted, suffix); } // ─── Marquee (horizontal, infinite, content-driven) ──────────────────── function Marquee({ items, renderItem, gap = 56, duration = 60, className = "" }) { const doubled = [...items, ...items]; return React.createElement( "div", { className: `marquee ${className}` }, React.createElement( "div", { className: "marquee-track", style: { gap: `${gap}px`, animationDuration: `${duration}s` }, }, doubled.map((it, i) => React.createElement(React.Fragment, { key: i }, renderItem(it, i))) ) ); } // ─── Magnetic button effect ──────────────────────────────────────────── function useMagnetic(ref, strength = 0.25) { useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; const dx = (e.clientX - cx) * strength; const dy = (e.clientY - cy) * strength; el.style.transform = `translate(${dx}px, ${dy}px)`; }; const onLeave = () => { el.style.transform = ""; }; el.addEventListener("mousemove", onMove); el.addEventListener("mouseleave", onLeave); return () => { el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); }; }, [ref, strength]); } function MagBtn({ href, variant = "primary", size = "md", children, className = "" }) { const ref = useRef(null); const inner = useRef(null); useMagnetic(ref, 0.18); useMagnetic(inner, 0.10); const cls = `btn btn-${variant} ${size === "sm" ? "btn-sm" : ""} ${className}`; return React.createElement( "a", { href, ref, className: cls, target: "_blank", rel: "noopener noreferrer", "data-magnetic": true }, React.createElement("span", { ref: inner, style: { display: "inline-flex", alignItems: "center", gap: 10 } }, children) ); } // ─── WhatsApp icon ───────────────────────────────────────────────────── const WaIcon = ({ size = 16 }) => React.createElement( "svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true" }, React.createElement("path", { d: "M17.6 6.32A7.92 7.92 0 0 0 12.05 4C7.6 4 4 7.6 4 12.04c0 1.42.38 2.8 1.08 4.02L4 20l4.06-1.06a8.05 8.05 0 0 0 3.99 1.02h.01c4.43 0 8.04-3.6 8.04-8.04a7.97 7.97 0 0 0-2.5-5.6Zm-5.55 12.36h-.01a6.69 6.69 0 0 1-3.4-.93l-.24-.14-2.41.63.65-2.35-.16-.25a6.65 6.65 0 0 1 10.31-8.2 6.6 6.6 0 0 1 1.95 4.7 6.69 6.69 0 0 1-6.69 6.54Zm3.66-5.01c-.2-.1-1.19-.59-1.37-.65-.18-.07-.32-.1-.45.1-.13.2-.51.65-.62.78-.12.13-.23.15-.42.05-.2-.1-.84-.31-1.6-.99a6.04 6.04 0 0 1-1.11-1.38c-.12-.2-.01-.3.09-.4.09-.09.2-.23.3-.35.1-.12.13-.2.2-.33.06-.13.03-.25-.02-.35-.05-.1-.45-1.09-.62-1.49-.16-.39-.33-.34-.45-.34l-.39-.01a.74.74 0 0 0-.54.25c-.18.2-.7.68-.7 1.66 0 .98.72 1.93.82 2.06.1.13 1.41 2.16 3.43 3.03.48.2.85.33 1.14.42.48.15.92.13 1.26.08.38-.06 1.19-.49 1.36-.96.17-.47.17-.87.12-.96-.05-.09-.18-.14-.38-.24Z", }) ); // ─── Arrow icon ──────────────────────────────────────────────────────── const ArrowIcon = ({ size = 14 }) => React.createElement( "svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" }, React.createElement("path", { d: "M5 12h14M13 6l6 6-6 6", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", }) ); // Export to window Object.assign(window, { CursorDot, useReveal, AnimatedNumber, Marquee, MagBtn, WaIcon, ArrowIcon, useMagnetic, });