// shared.jsx - reusable primitives for all sections (exported to window).

// ── In-view detection, optimized ──────────────────────────────────────────
// ONE shared IntersectionObserver + ONE shared (rAF-coalesced, self-pruning)
// scroll/resize fallback for every reveal on the page — instead of dozens of
// per-element listeners each calling getBoundingClientRect on every scroll.
// The IO is the fast path; the scroll fallback only does work while elements
// are still pending and stops entirely once everything has revealed.
const _pending = new Set();
let _io = null, _scrollBound = false, _flushQueued = false;

function _reveal(item) {
  item.cb(true);
  if (item.once) { _pending.delete(item); if (_io) _io.unobserve(item.el); _ioMap.delete(item.el); }
}
const _ioMap = new Map();
function _getIO() {
  if (!_io) {
    _io = new IntersectionObserver((entries) => {
      for (const e of entries) {
        const item = _ioMap.get(e.target);
        if (!item) continue;
        if (e.isIntersecting) _reveal(item);
        else if (!item.once) item.cb(false);
      }
    }, { threshold: 0.15 });
  }
  return _io;
}
function _flush() {
  _flushQueued = false;
  if (_pending.size === 0) return;
  const vh = window.innerHeight || document.documentElement.clientHeight;
  for (const item of [..._pending]) {
    const r = item.el.getBoundingClientRect();
    if (r.top < vh * 0.92 && r.bottom > vh * 0.08) _reveal(item);
  }
}
// Coalesced via a timer (not rAF) so it still fires when the tab/iframe is
// unfocused and rAF is throttled. ~16fps cap, and it self-stops once nothing
// is pending.
function _queueFlush() {
  if (_flushQueued || _pending.size === 0) return;
  _flushQueued = true;
  setTimeout(_flush, 60);
}
function _ensureScroll() {
  if (_scrollBound) return;
  _scrollBound = true;
  window.addEventListener('scroll', _queueFlush, { passive: true });
  window.addEventListener('resize', _queueFlush, { passive: true });
}

function useInView(opts) {
  const { once = true } = opts || {};
  const ref = React.useRef(null);
  const [inView, setInView] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const item = { el, once, cb: setInView };
    _ioMap.set(el, item);
    if (once) _pending.add(item);
    _getIO().observe(el);
    _ensureScroll();
    _queueFlush();
    return () => {
      _pending.delete(item);
      _ioMap.delete(el);
      if (_io) _io.unobserve(el);
    };
  }, []);
  return [ref, inView];
}

// Reveal-on-scroll wrapper. `as` picks the tag; `delay` staggers.
const Reveal = React.forwardRef(function Reveal(
  { children, delay = 0, as = 'div', className = '', style = {}, threshold = 0.2, ...rest }, _ref
) {
  const [ref, inView] = useInView({ threshold });
  const Comp = as;
  // Merge the internal observer ref with any forwarded ref so callers can
  // both observe (count-ups, etc.) and read the node.
  const setRefs = React.useCallback((node) => {
    ref.current = node;
    if (typeof _ref === 'function') _ref(node);
    else if (_ref) _ref.current = node;
  }, [_ref]);
  return (
    <Comp ref={setRefs} className={`ios ${inView ? 'in' : ''} ${className}`}
      style={{ '--d': `${delay}ms`, ...style }} {...rest}>
      {children}
    </Comp>
  );
});

function useCountUp(target, start, duration = 1100, delay = 0) {
  const [val, setVal] = React.useState(0);
  React.useEffect(() => {
    if (!start) return;
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setVal(target); return; }
    let raf, t0;
    const tick = (now) => {
      if (!t0) t0 = now;
      const el = now - t0 - delay;
      if (el < 0) { raf = requestAnimationFrame(tick); return; }
      const p = Math.min(1, el / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(eased * target);
      if (p < 1) raf = requestAnimationFrame(tick);
      else setVal(target);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [start]);
  return val;
}

// Section header: eyebrow + title (children) + optional subtitle, revealed.
function SectionHead({ eyebrow, children, sub, align = 'left', max = 700, style = {} }) {
  const center = align === 'center';
  return (
    <div style={{ maxWidth: max, marginInline: center ? 'auto' : undefined, textAlign: align, marginBottom: 56, ...style }}>
      <Reveal as="p" className="eyebrow">{eyebrow}</Reveal>
      <Reveal as="h2" className="sec-title" delay={90}>{children}</Reveal>
      {sub && (
        <Reveal as="p" delay={160} style={{
          fontSize: 'clamp(0.95rem, 1.3vw, 1.1rem)', lineHeight: 1.55,
          color: 'var(--fg-muted)', margin: 'clamp(8px, 1.4vh, 18px) 0 0', maxWidth: 560,
          marginInline: center ? 'auto' : undefined,
        }}>{sub}</Reveal>
      )}
    </div>
  );
}

Object.assign(window, { useInView, Reveal, useCountUp, SectionHead });
