/* Apple-style scroll animations: fade-up reveals, hero pin/parallax,
   number-on-scroll. Intersection observer + scroll listener. */

function useReveal() {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    // Synchronous initial-paint check: if the element is already on screen
    // when mounted (above-the-fold), reveal it immediately. The IO callback
    // can race with first layout in Babel-transpiled trees and sometimes
    // never fires for in-viewport elements until the user scrolls.
    const checkNow = () => {
      if (!ref.current) return;
      const r = ref.current.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      // Match the IO's effective bounds: visible if any part is between
      // top: 0 and bottom: 90vh.
      setShown(r.top < vh * 0.9 && r.bottom > 0);
    };
    checkNow();
    const raf = requestAnimationFrame(() => { checkNow(); });
    // Bidirectional: reveal when entering viewport, un-reveal when leaving,
    // so the animation replays on both scroll-down and scroll-up.
    // Asymmetric rootMargin: bottom is pulled inward (-10%) so reveal kicks in
    // a bit before the element reaches the bottom edge; top is left at 0 so
    // we don't un-reveal until the element is fully above the viewport.
    // Otherwise large headings flicker as they approach the sticky nav,
    // because the un-reveal animation slides them DOWN while the scroll is
    // moving them UP.
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        setShown(e.isIntersecting);
      });
    }, { threshold: 0, rootMargin: '0px 0px -10% 0px' });
    io.observe(ref.current);
    return () => { io.disconnect(); cancelAnimationFrame(raf); };
  }, []);
  return [ref, shown];
}

function Reveal({ children, delay = 0, y = 56, scale = 0.96, as: Tag = 'div', style, ...rest }) {
  const [ref, shown] = useReveal();
  const tx = shown ? 'none' : `translate3d(0, ${y}px, 0) scale(${scale})`;
  return (
    <Tag
      ref={ref}
      style={{
        opacity: shown ? 1 : 0,
        transform: tx,
        transition: `opacity 1.1s cubic-bezier(.16,.84,.32,1) ${delay}s, transform 1.2s cubic-bezier(.16,.84,.32,1) ${delay}s`,
        willChange: 'opacity, transform',
        ...style,
      }}
      {...rest}
    >
      {children}
    </Tag>
  );
}

/* useScrollProgress(ref): returns 0→1 as the element scrolls from
   "just entered viewport" to "just left viewport top". Apple-style pin range.
   Calls onTick(p) every animation frame instead of triggering React re-renders
   so 60fps scroll updates don't spam the reconciler. */
function useScrollProgress(ref, onTick, { startOffset = 0.85, endOffset = 0.0 } = {}) {
  // Pin onTick in a ref so callers can pass an inline arrow without
  // re-binding scroll listeners every render (which caused jitter).
  const cb = React.useRef(onTick);
  React.useEffect(() => { cb.current = onTick; }, [onTick]);

  React.useEffect(() => {
    if (!ref.current) return;
    let raf = 0;
    let lastP = -1;
    const update = () => {
      raf = 0;
      if (!ref.current) return;
      const r = ref.current.getBoundingClientRect();
      const vh = window.innerHeight;
      const start = startOffset * vh;
      const end = endOffset * vh - r.height;
      const raw = (start - r.top) / (start - end);
      const p = Math.max(0, Math.min(1, raw));
      // Skip identical updates so we don't thrash the DOM at 60fps
      // when scrolled past the section (p stuck at 0 or 1).
      if (Math.abs(p - lastP) < 0.001) return;
      lastP = p;
      cb.current(p);
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    update();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      cancelAnimationFrame(raf);
    };
  }, [ref, startOffset, endOffset]);
}

/* lerp helper */
const lerp = (a, b, t) => a + (b - a) * t;
const clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
const easeOut = t => 1 - Math.pow(1 - t, 3);

/* useIsMobile: returns true when viewport width is below the breakpoint.
   Used to swap multi-column grids to stacked layouts on phones. */
function useIsMobile(breakpoint = 768) {
  const get = () => typeof window !== 'undefined' && window.innerWidth < breakpoint;
  const [isMobile, setIsMobile] = React.useState(get);
  React.useEffect(() => {
    const onResize = () => setIsMobile(get());
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [breakpoint]);
  return isMobile;
}

window.Reveal = Reveal;
window.useReveal = useReveal;
window.useScrollProgress = useScrollProgress;
window.useIsMobile = useIsMobile;
window.lerp = lerp;
window.clamp = clamp;
window.easeOut = easeOut;
