// Nexora Cores — Website UI Kit · Motion helpers (Reveal + CountUp)
(function () {
  const { useRef, useState, useEffect } = React;
  const reduce = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const scrollRoot = () => document.querySelector('[data-scroll-root]') || null;

  // Rise-up on scroll into view. Transform-only (content stays fully opaque),
  // so a frozen transition in a throttled tab can never leave it invisible.
  function Reveal({ children, delay = 0, y = 26, style = {}, className }) {
    const ref = useRef(null);
    const [shown, setShown] = useState(false);
    useEffect(() => {
      const el = ref.current;
      if (!el || reduce()) { setShown(true); return; }
      const io = new IntersectionObserver(([e]) => {
        if (e.isIntersecting) { setShown(true); io.disconnect(); }
      }, { threshold: 0.12, root: scrollRoot(), rootMargin: '0px 0px -8% 0px' });
      io.observe(el);
      // Fallback: never stay in the pre-state if IO never fires.
      const t = setTimeout(() => setShown(true), 2500);
      return () => { io.disconnect(); clearTimeout(t); };
    }, []);
    return React.createElement('div', {
      ref, className,
      style: {
        ...style,
        transform: shown ? 'none' : `translateY(${y}px)`,
        transition: `transform .75s var(--ease-out) ${delay}s`,
        willChange: 'transform',
      },
    }, children);
  }

  // Animated number that counts up to `value` (a string like "$91K", "60M", "68%", "247").
  function CountUp({ value, duration = 1400, start = 'view', style = {} }) {
    const ref = useRef(null);
    const m = String(value).match(/^(\D*)([\d.,]+)(.*)$/);
    const [txt, setTxt] = useState(m ? m[1] + '0' + m[3] : value);
    useEffect(() => {
      if (!m) { setTxt(value); return; }
      const prefix = m[1], suffix = m[3];
      const target = parseFloat(m[2].replace(/,/g, ''));
      const decimals = (m[2].split('.')[1] || '').length;
      const fmt = n => prefix + (decimals ? n.toFixed(decimals) : Math.round(n).toLocaleString()) + suffix;
      if (reduce()) { setTxt(fmt(target)); return; }
      let raf, t0;
      const run = () => {
        const step = ts => {
          if (!t0) t0 = ts;
          const p = Math.min((ts - t0) / duration, 1);
          const eased = 1 - Math.pow(1 - p, 3);
          setTxt(fmt(target * eased));
          if (p < 1) raf = requestAnimationFrame(step);
        };
        raf = requestAnimationFrame(step);
      };
      if (start === 'view' && ref.current) {
        const io = new IntersectionObserver(([e]) => {
          if (e.isIntersecting) { run(); io.disconnect(); }
        }, { threshold: 0.4, root: scrollRoot() });
        io.observe(ref.current);
        return () => { io.disconnect(); cancelAnimationFrame(raf); };
      }
      run();
      return () => cancelAnimationFrame(raf);
    }, [value]);
    return React.createElement('span', { ref, style }, txt);
  }

  window.Reveal = Reveal;
  window.CountUp = CountUp;
})();
