/* utils.jsx — shared hooks + small components. Exports to window. */
const { useState, useEffect, useRef, useCallback } = React;

/* Animate a number from 0 → end when scrolled into view */
function CountUp({ end, duration = 1600, decimals = 0, prefix = "", suffix = "", className = "" }) {
  const [val, setVal] = useState(0);
  const ref = useRef(null);
  const done = useRef(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const run = () => {
      if (done.current) return;
      done.current = true;
      const start = performance.now();
      const tick = (now) => {
        const p = Math.min(1, (now - start) / duration);
        const eased = 1 - Math.pow(1 - p, 3);
        setVal(end * eased);
        if (p < 1) requestAnimationFrame(tick);
        else setVal(end);
      };
      requestAnimationFrame(tick);
      // Guarantee the final value lands even if rAF is throttled/paused.
      setTimeout(() => setVal(end), duration + 120);
    };
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) run(); });
    }, { threshold: 0.4 });
    io.observe(el);
    // Fallback: ensure the number reaches its value even if the observer
    // never fires (e.g. below the fold in a non-scrolling frame).
    const fb = setTimeout(run, 1600);
    return () => { io.disconnect(); clearTimeout(fb); };
  }, [end, duration]);
  const display = decimals > 0
    ? val.toFixed(decimals)
    : Math.round(val).toLocaleString("en-US");
  return <span ref={ref} className={"tnum " + className}>{prefix}{display}{suffix}</span>;
}

/* Magnetic hover effect for buttons */
function useMagnetic(strength = 0.32) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    if (window.matchMedia("(hover: none)").matches) return;
    const move = (e) => {
      const r = el.getBoundingClientRect();
      const x = e.clientX - (r.left + r.width / 2);
      const y = e.clientY - (r.top + r.height / 2);
      el.style.transform = `translate(${x * strength}px, ${y * strength}px)`;
    };
    const reset = () => { el.style.transform = ""; };
    el.addEventListener("mousemove", move);
    el.addEventListener("mouseleave", reset);
    return () => { el.removeEventListener("mousemove", move); el.removeEventListener("mouseleave", reset); };
  }, [strength]);
  return ref;
}

/* Reveals are now driven by a load-time CSS animation (see .reveal in
   styles.css), so no scroll observer is required. Kept as a no-op so the
   call site stays stable. */
function useRevealObserver() {}

/* Cycle through words on an interval */
function useRotator(words, interval = 2200) {
  const [i, setI] = useState(0);
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const t = setInterval(() => setI((p) => (p + 1) % words.length), interval);
    return () => clearInterval(t);
  }, [words.length, interval]);
  return [i, words[i]];
}

Object.assign(window, { CountUp, useMagnetic, useRevealObserver, useRotator });
