// components.jsx — reusable bits shared across sections.

const { useState, useEffect, useRef, useLayoutEffect, useMemo, useCallback } = React;

// ── Reveal: simple pass-through wrapper. CSS used to animate this, but iframe
// visibility throttling made the transitions flaky. The site has enough motion
// from the ambient mesh + sun glow that per-element fades aren't missed.
function Reveal({ children, delay = 0, as = "div", className = "", ...rest }) {
  const Tag = as;
  const cls = `reveal ${delay ? `d${delay}` : ""} ${className}`.replace(/\s+/g, " ").trim();
  return <Tag className={cls} {...rest}>{children}</Tag>;
}

// ── Hook: track scroll progress through an element (0..1 as it moves through viewport)
function useScrollProgress(ref, opts = {}) {
  const { start = 0, end = 1 } = opts; // start: enter-from-bottom, end: leave-out-top
  const [p, setP] = useState(0);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const r = el.getBoundingClientRect();
        const vh = window.innerHeight;
        // Map from element top entering bottom of viewport to element bottom leaving top.
        const total = r.height + vh;
        const passed = vh - r.top;
        let prog = passed / total;
        prog = (prog - start) / (end - start);
        setP(Math.max(0, Math.min(1, prog)));
      });
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, [start, end]);
  return p;
}

// ── Placeholder image (with subject label)
function Placeholder({ label, ratio = "16/9", style, children, className = "", showCross = true }) {
  return (
    <div className={`ph ${showCross ? "ph-crosshair" : ""} ${className}`} style={{ aspectRatio: ratio, ...style }}>
      {label && <div className="ph-label">{label}</div>}
      {children}
    </div>
  );
}

// ── Tiny inline SVGs (no icon font)
const Glyph = {
  arrow: (p) => (<svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M3 8h9.5M9 4.5 12.5 8 9 11.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  plus: (p) => (<svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>),
  dot: (p) => (<svg viewBox="0 0 8 8" width="8" height="8" {...p}><circle cx="4" cy="4" r="3" fill="currentColor"/></svg>),
  logo: (p) => (
    // Cactus mark — simple, geometric, Apple-clean
    <svg viewBox="0 0 24 24" width="20" height="20" {...p} aria-hidden="true">
      <path d="M9 21V10a3 3 0 0 1 6 0v11" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" fill="none"/>
      <path d="M6 14v-2a2 2 0 0 1 2-2h1M18 13v-3a2 2 0 0 0-2-2h-1" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" fill="none"/>
      <path d="M7 21h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
    </svg>
  ),
};

// ── Number ticker (counts up when in view; falls back to immediate final value)
function Ticker({ to, suffix = "", duration = 1400, fmt = (n) => Math.round(n).toLocaleString() }) {
  const ref = useRef(null);
  const [n, setN] = useState(to); // start at final value so we never paint "0"
  useEffect(() => {
    const el = ref.current; if (!el) return;
    let raf = 0; let started = false; let t0 = 0;
    setN(0); // reset to 0 for the count-up animation
    const tick = (t) => {
      if (!t0) t0 = t;
      const p = Math.min(1, (t - t0) / duration);
      const e = 1 - Math.pow(1 - p, 3);
      setN(to * e);
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    const start = () => { if (started) return; started = true; raf = requestAnimationFrame(tick); };
    // Synchronous in-view check
    const r = el.getBoundingClientRect();
    const vh = window.innerHeight || document.documentElement.clientHeight;
    if (r.top < vh * 0.95 && r.bottom > 0) start();
    // IO for below-the-fold
    let io;
    try {
      io = new IntersectionObserver((entries) => {
        entries.forEach((e) => { if (e.isIntersecting) { start(); io.unobserve(el); } });
      }, { threshold: 0.4 });
      io.observe(el);
    } catch (_) {}
    // Safety net — snap to final value if nothing fires
    const tid = setTimeout(() => { if (!started) { started = true; setN(to); } }, 1500);
    return () => { cancelAnimationFrame(raf); if (io) io.disconnect(); clearTimeout(tid); };
  }, [to, duration]);
  return <span ref={ref}>{fmt(n)}{suffix}</span>;
}

// ── Magnetic button (subtle Apple-ish pull on hover)
function MagButton({ children, primary, onClick, href, style }) {
  const ref = useRef(null);
  const onMove = (e) => {
    const el = ref.current; if (!el) return;
    const r = el.getBoundingClientRect();
    const x = (e.clientX - (r.left + r.width / 2)) * 0.18;
    const y = (e.clientY - (r.top + r.height / 2)) * 0.18;
    el.style.transform = `translate(${x}px, ${y}px)`;
  };
  const onLeave = () => { if (ref.current) ref.current.style.transform = ""; };
  const cls = primary ? "btn btn-primary" : "btn btn-ghost";
  const Tag = href ? "a" : "button";
  return (
    <Tag ref={ref} className={cls} href={href} onClick={onClick}
         onMouseMove={onMove} onMouseLeave={onLeave} style={style}>
      {children}
    </Tag>
  );
}

// ── Typing flipper — types each word in, pauses, deletes, types the next
function WordTyper({ words, typeDelay = 80, deleteDelay = 45, holdDelay = 1600, eraseHold = 220 }) {
  const [text, setText] = useState("");
  const [i, setI] = useState(0);
  const [phase, setPhase] = useState("typing"); // typing | holding | deleting | pausing
  useEffect(() => {
    if (!words || !words.length) return;
    const current = words[i];
    let timer;
    if (phase === "typing") {
      if (text.length < current.length) {
        timer = setTimeout(() => setText(current.slice(0, text.length + 1)), typeDelay);
      } else {
        timer = setTimeout(() => setPhase("deleting"), holdDelay);
      }
    } else if (phase === "deleting") {
      if (text.length > 0) {
        timer = setTimeout(() => setText(current.slice(0, text.length - 1)), deleteDelay);
      } else {
        timer = setTimeout(() => {
          setI((p) => (p + 1) % words.length);
          setPhase("typing");
        }, eraseHold);
      }
    }
    return () => clearTimeout(timer);
  }, [text, phase, i, words, typeDelay, deleteDelay, holdDelay, eraseHold]);
  // Reserve enough width for the longest word so the layout doesn't jiggle.
  const longest = words.reduce((a, b) => (b.length > a.length ? b : a), "");
  return (
    <span style={{ position:"relative", display:"inline-block", verticalAlign:"baseline", whiteSpace:"nowrap" }}>
      <span aria-hidden="true" style={{ visibility:"hidden" }}>{longest}</span>
      <span style={{ position:"absolute", left:0, top:0 }}>
        {text}
        <span style={{
          display:"inline-block", width:"0.06em",
          height:"0.92em", verticalAlign:"-0.12em",
          background:"currentColor", marginLeft:"0.06em",
          animation:"caretBlink 1s steps(1) infinite",
        }}/>
      </span>
      <style>{`@keyframes caretBlink{0%,49%{opacity:1}50%,100%{opacity:0}}`}</style>
    </span>
  );
}

Object.assign(window, { WordTyper });
Object.assign(window, { Reveal, useScrollProgress, Placeholder, Glyph, Ticker, MagButton });
