/* ══════════════════════════════════════════════════
   HOOKS — the scroll machinery behind the narrative.
══════════════════════════════════════════════════ */

/* Read once: with reduced motion every scene reports its final,
   fully-revealed state instead of scrubbing. */
const REDUCED_MOTION = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/* ── useInView: reveals a section once it scrolls into the viewport ── */
const useInView = (threshold = 0.12) => {
  const ref = useRef(null);
  const [vis, setVis] = useState(false);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVis(true); obs.unobserve(el); } }, { threshold });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  return [ref, vis];
};

/* ── useOnScreen: continuous visibility, for pausing animations ──
   Unlike useInView this flips back to false when the element leaves,
   so rAF loops (the route map) only run while actually watchable. */
const useOnScreen = () => {
  const ref = useRef(null);
  const [on, setOn] = useState(false);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    const obs = new IntersectionObserver(([e]) => setOn(e.isIntersecting), { threshold: 0.05 });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  return [ref, on];
};

/* ── useScene: a scroll-scrubbed sticky chapter ──
   The section is taller than the viewport (.scene) and its inner stage
   is position:sticky. Progress through the extra height becomes two
   things with very different costs:

   • `--p` [0..1], written straight onto the section element every
     animation frame. CSS consumes it (trail drawing, parallax) with
     ZERO React re-renders — this is what keeps scrubbing at 60fps.

   • `step`, the quantized phase (0..steps-1). Only phase CHANGES
     re-render, so text highlights and layer reveals stay cheap.

   Reduced motion normally pins the scene to its final phase. Product
   walkthroughs may opt to keep their discrete scroll steps while CSS
   removes the animated transitions. */
const useScene = (steps = 1, options = {}) => {
  const keepStepsInReducedMotion = Boolean(options.keepStepsInReducedMotion);
  const reduceToFinal = REDUCED_MOTION && !keepStepsInReducedMotion;
  const ref = useRef(null);
  const [step, setStep] = useState(reduceToFinal ? steps - 1 : 0);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    if (reduceToFinal) { el.style.setProperty('--p', 1); return; }
    let raf = 0, last = -1;
    const frame = () => {
      raf = 0;
      const r = el.getBoundingClientRect();
      const span = r.height - window.innerHeight;
      const p = span > 40 ? Math.min(1, Math.max(0, -r.top / span)) : (r.top < 0 ? 1 : 0);
      el.style.setProperty('--p', p.toFixed(4));
      const s = Math.min(steps - 1, Math.floor(p * steps));
      if (s !== last) { last = s; setStep(s); }
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(frame); };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [steps, reduceToFinal]);
  return [ref, step];
};
