// motion.jsx — shared hooks + behaviors for the Plumb landing page
// Exposes window.useReveal, window.useStagger, window.useParallax, window.useScrollSpy,
// window.attachRipple, window.smoothScrollTo, window.useCountUp

(function () {
  const { useEffect, useRef, useState } = React;

  // ── Generic IntersectionObserver hook ───────────────────────────────────
  function useReveal(selector = '.reveal-fx', threshold = 0.12) {
    useEffect(() => {
      const els = Array.from(document.querySelectorAll(selector));
      if (!els.length) return;
      const io = new IntersectionObserver((entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add('in');
            io.unobserve(e.target);
          }
        });
      }, { threshold, rootMargin: '0px 0px -8% 0px' });
      els.forEach((el) => io.observe(el));
      return () => io.disconnect();
    }, [selector, threshold]);
  }

  // ── Stagger reveal ──────────────────────────────────────────────────────
  function useStagger(selector = '.stagger') {
    useEffect(() => {
      const els = Array.from(document.querySelectorAll(selector));
      if (!els.length) return;
      const io = new IntersectionObserver((entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add('in');
            io.unobserve(e.target);
          }
        });
      }, { threshold: 0.15, rootMargin: '0px 0px -6% 0px' });
      els.forEach((el) => io.observe(el));
      return () => io.disconnect();
    }, [selector]);
  }

  // ── Parallax — drives CSS vars on declared elements via rAF ─────────────
  // Bumped 4–5× for real depth separation. Background grid drifts slowest,
  // bob descends fastest (it's tied to gravity in the brand metaphor),
  // corners crawl along.
  function useParallax(intensity = 1) {
    useEffect(() => {
      const root = document.documentElement.style;
      if (intensity === 0) {
        ['--py-grid','--py-bob','--py-corners','--py-far','--py-mid','--py-near','--py-bob-line'].forEach(v => root.setProperty(v, '0px'));
        root.setProperty('--bob-fall', '0');
        return;
      }
      let raf = null;
      const tick = () => {
        const y = window.scrollY;
        const vh = window.innerHeight || 800;
        // Hero-anchored progress (0 → 1.5 across the hero zone, allows overshoot)
        const heroProg = Math.max(0, Math.min(1.5, y / (vh * 0.95)));

        // Background grid: slow, atmospheric drift
        root.setProperty('--py-grid', (-y * 0.32 * intensity) + 'px');
        // Hero bob STAGE drifts up with scroll (gives the impression bob sinks faster)
        root.setProperty('--py-bob', (-y * 0.18 * intensity) + 'px');
        // Corners crawl
        root.setProperty('--py-corners', (-y * 0.45 * intensity) + 'px');

        // Multi-depth layers for non-hero sections
        root.setProperty('--py-far', (-y * 0.55 * intensity) + 'px');
        root.setProperty('--py-mid', (-y * 0.28 * intensity) + 'px');
        root.setProperty('--py-near', (-y * 0.12 * intensity) + 'px');

        // Bob line gravity — within hero, bob falls AND line extends as you scroll.
        // 0 = settled (intro animation end), 1 = fully extended at hero bottom.
        // Continues a touch past 1 (slight overshoot/sway) before exiting view.
        root.setProperty('--bob-fall', heroProg.toFixed(4));
        // Tiny extra translate so the bob keeps physical descent past the line's full length
        root.setProperty('--py-bob-line', (heroProg > 1 ? (heroProg - 1) * 80 : 0).toFixed(2) + 'px');

        raf = null;
      };
      const onScroll = () => { if (raf == null) raf = requestAnimationFrame(tick); };
      window.addEventListener('scroll', onScroll, { passive: true });
      window.addEventListener('resize', onScroll, { passive: true });
      tick();
      return () => {
        window.removeEventListener('scroll', onScroll);
        window.removeEventListener('resize', onScroll);
        if (raf) cancelAnimationFrame(raf);
      };
    }, [intensity]);
  }

  // ── Pin-and-scrub — element receives a CSS var with progress 0..1 ───────
  // Wrap a scene container in an outer "pin track" of e.g. 300vh and set the
  // inner element to position: sticky; top: 0; height: 100vh. This hook
  // computes 0..1 across the track and sets data-progress + a CSS var.
  function usePinScrub(selector, opts = {}) {
    const { onProgress } = opts;
    useEffect(() => {
      const tracks = Array.from(document.querySelectorAll(selector));
      if (!tracks.length) return;
      let raf = null;
      const tick = () => {
        const vh = window.innerHeight || 800;
        tracks.forEach((track) => {
          const r = track.getBoundingClientRect();
          // Progress: 0 when track top hits viewport top, 1 when track bottom is one vh above
          const total = Math.max(1, r.height - vh);
          const scrolled = Math.max(0, Math.min(total, -r.top));
          const p = scrolled / total;
          track.style.setProperty('--scrub', p.toFixed(4));
          track.setAttribute('data-progress', p.toFixed(3));
          if (onProgress) onProgress(track, p);
        });
        raf = null;
      };
      const onScroll = () => { if (raf == null) raf = requestAnimationFrame(tick); };
      window.addEventListener('scroll', onScroll, { passive: true });
      window.addEventListener('resize', onScroll, { passive: true });
      tick();
      return () => {
        window.removeEventListener('scroll', onScroll);
        window.removeEventListener('resize', onScroll);
        if (raf) cancelAnimationFrame(raf);
      };
    }, [selector]);
  }

  // ── Scroll-spy for the right-rail nav ───────────────────────────────────
  function useScrollSpy(ids) {
    const [active, setActive] = useState(ids[0]);
    useEffect(() => {
      const els = ids.map((id) => document.getElementById(id)).filter(Boolean);
      if (!els.length) return;
      const io = new IntersectionObserver((entries) => {
        const visible = entries
          .filter((e) => e.isIntersecting)
          .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
        if (visible) setActive(visible.target.id);
      }, { threshold: [0.18, 0.4, 0.6], rootMargin: '-12% 0px -50% 0px' });
      els.forEach((el) => io.observe(el));
      return () => io.disconnect();
    }, [ids.join(',')]);
    return active;
  }

  // ── Smooth scroll with easing override (for buttons/nav) ────────────────
  function smoothScrollTo(target, offset = 24) {
    const el = typeof target === 'string' ? document.getElementById(target) : target;
    if (!el) return;
    const y = el.getBoundingClientRect().top + window.scrollY - offset;
    window.scrollTo({ top: y, behavior: 'smooth' });
  }

  // ── Ripple click handler — attach via onClick or via attachRipple(ref) ──
  function attachRipple(e) {
    const btn = e.currentTarget;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    const size = Math.max(r.width, r.height) * 1.2;
    const x = (e.clientX || (r.left + r.width / 2)) - r.left;
    const y = (e.clientY || (r.top + r.height / 2)) - r.top;
    const rip = document.createElement('span');
    rip.className = 'rip';
    rip.style.width = rip.style.height = size + 'px';
    rip.style.left = (x - size / 2) + 'px';
    rip.style.top = (y - size / 2) + 'px';
    btn.appendChild(rip);
    setTimeout(() => rip.remove(), 700);
  }

  // ── Magnetic hover — gentle cursor pull on element ──────────────────────
  function useMagnet(ref, strength = 0.18) {
    useEffect(() => {
      const el = ref.current;
      if (!el) return;
      const onMove = (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 onLeave = () => { el.style.transform = ''; };
      el.addEventListener('mousemove', onMove);
      el.addEventListener('mouseleave', onLeave);
      return () => {
        el.removeEventListener('mousemove', onMove);
        el.removeEventListener('mouseleave', onLeave);
      };
    }, [ref, strength]);
  }

  // ── Count-up — animate a number from->to when revealed ──────────────────
  function useCountUp(target, opts = {}) {
    const { from = 0, duration = 900, decimals = 2, prefix = '', suffix = '' } = opts;
    const ref = useRef(null);
    useEffect(() => {
      const el = ref.current;
      if (!el) return;
      let raf = null;
      let started = false;
      const run = () => {
        const t0 = performance.now();
        const tick = (now) => {
          const p = Math.min(1, (now - t0) / duration);
          // easeOutCubic
          const eased = 1 - Math.pow(1 - p, 3);
          const v = from + (target - from) * eased;
          el.textContent = prefix + v.toFixed(decimals) + suffix;
          if (p < 1) raf = requestAnimationFrame(tick);
        };
        raf = requestAnimationFrame(tick);
      };
      const io = new IntersectionObserver((entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting && !started) { started = true; run(); io.disconnect(); }
        });
      }, { threshold: 0.4 });
      io.observe(el);
      return () => { io.disconnect(); if (raf) cancelAnimationFrame(raf); };
    }, [target, from, duration, decimals, prefix, suffix]);
    return ref;
  }

  // ── CountUp inline component — drop-in for stat numbers ─────────────────
  // Usage: <CountUp to={40000} prefix="$" thousands /> or
  //        <CountUp to={8} suffix=" min" />
  //        <CountUp to={-1.30} prefix="$" decimals={2} signed />
  function CountUp({
    to, from = 0, decimals = 0, prefix = '', suffix = '',
    duration = 1100, thousands = false, signed = false, className = '',
  }) {
    const ref = useRef(null);
    const [val, setVal] = useState(from);
    useEffect(() => {
      const el = ref.current;
      if (!el) return;
      let raf = null;
      let started = false;
      const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      const run = () => {
        if (reduced) { setVal(to); return; }
        const t0 = performance.now();
        const tick = (now) => {
          const p = Math.min(1, (now - t0) / duration);
          const eased = 1 - Math.pow(1 - p, 3);
          setVal(from + (to - from) * eased);
          if (p < 1) raf = requestAnimationFrame(tick);
        };
        raf = requestAnimationFrame(tick);
      };
      const io = new IntersectionObserver((entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting && !started) { started = true; run(); io.disconnect(); }
        });
      }, { threshold: 0.4 });
      io.observe(el);
      return () => { io.disconnect(); if (raf) cancelAnimationFrame(raf); };
    }, [to, from, duration]);

    const fmt = (n) => {
      const abs = Math.abs(n);
      let s = abs.toFixed(decimals);
      if (thousands) {
        const [whole, frac] = s.split('.');
        s = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + (frac ? '.' + frac : '');
      }
      const sign = signed ? (n < 0 ? '−' : '+') : (n < 0 ? '−' : '');
      return sign + prefix + s + suffix;
    };
    return React.createElement('span', { ref, className: 'count-up ' + className }, fmt(val));
  }

  Object.assign(window, {
    useReveal, useStagger, useParallax, usePinScrub, useScrollSpy,
    smoothScrollTo, attachRipple, useMagnet, useCountUp, CountUp,
  });
})();
