/* ============================================================
   EDITORIAL INDEX — components + hooks
   ============================================================ */
const { useState, useEffect, useRef, useCallback } = React;

/* scroll-driven in-view (reliable across embeds) + safety reveal */
function useInView(opts) {
  const ref = useRef(null);
  const [seen, setSeen] = useState(false);
  const margin = (opts && opts.margin != null) ? opts.margin : 0.92;
  useEffect(() => {
    const el = ref.current; if (!el) return;
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setSeen(true); return; }
    let done = false;
    const check = () => {
      if (done) return;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      if (r.top < vh * margin && r.bottom > 0) {
        done = true; setSeen(true);
        window.removeEventListener("scroll", check); window.removeEventListener("resize", check);
      }
    };
    const r1 = requestAnimationFrame(check);
    const r2 = requestAnimationFrame(() => requestAnimationFrame(check));
    window.addEventListener("scroll", check, { passive: true });
    window.addEventListener("resize", check);
    const safety = setTimeout(() => { if (!done) { done = true; setSeen(true); } }, 2600);
    return () => { cancelAnimationFrame(r1); cancelAnimationFrame(r2); clearTimeout(safety);
      window.removeEventListener("scroll", check); window.removeEventListener("resize", check); };
  }, []);
  return [ref, seen];
}

/* reveal wrapper — stamps rv-done shortly after to hard-lock end state */
function Reveal({ children, delay = 0, as = "div", className = "", style = {}, ...rest }) {
  const [ref, seen] = useInView();
  const [done, setDone] = useState(false);
  useEffect(() => { if (!seen) return; const id = setTimeout(() => setDone(true), 1400 + delay); return () => clearTimeout(id); }, [seen, delay]);
  const Tag = as;
  return (
    <Tag ref={ref}
      className={(className ? className + " " : "") + "reveal" + (seen ? " is-visible" : "") + (done ? " rv-done" : "")}
      style={{ transitionDelay: (seen ? delay : 0) + "ms", ...style }} {...rest}>
      {children}
    </Tag>
  );
}

/* clip-up reveal for display lines. `lines` = array of nodes/strings.
   each line wipes up from a mask; staggered. */
function ClipLines({ lines, className = "", style, lineStyle, stagger = 90, base = 0 }) {
  const [ref, seen] = useInView();
  const [done, setDone] = useState(false);
  useEffect(() => { if (!seen) return; const id = setTimeout(() => setDone(true), 2000); return () => clearTimeout(id); }, [seen]);
  return (
    <h1 ref={ref} className={"serif " + className + (seen ? " is-visible" : "") + (done ? " rv-done" : "")} style={style}>
      {lines.map((ln, i) => (
        <span key={i} className="clip-line" style={lineStyle}>
          <span className="clip"><span style={{ transitionDelay: (seen ? base + i * stagger : 0) + "ms" }}>{ln}</span></span>
        </span>
      ))}
    </h1>
  );
}

/* count-up */
function CountUp({ end, duration = 1300, suffix = "", className = "", style }) {
  const [ref, seen] = useInView({ margin: 0.82 });
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!seen) return;
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setVal(end); return; }
    let raf, start;
    const tick = (t) => { if (!start) start = t; const p = Math.min((t - start) / duration, 1);
      setVal(Math.round((1 - Math.pow(1 - p, 3)) * end)); if (p < 1) raf = requestAnimationFrame(tick); };
    raf = requestAnimationFrame(tick);
    const safety = setTimeout(() => setVal(end), duration + 400);
    return () => { cancelAnimationFrame(raf); clearTimeout(safety); };
  }, [seen, end]);
  return <span ref={ref} className={className} style={style}>{val}{suffix}</span>;
}

function Prose({ text, className = "body", style }) {
  return (
    <div className={className} style={style}>
      {String(text).split("\n\n").map((p, i) => <p key={i}>{p}</p>)}
    </div>
  );
}

/* striped placeholder */
function Placeholder({ label = "image", style, dark }) {
  return (
    <div className="ph" style={style}>
      <div className="ph-stripe"></div>
      <span className="ph-label folio">{label}</span>
    </div>
  );
}

/* infinite marquee ticker */
function Marquee({ items, sep = "✳", className = "" }) {
  const run = items.concat(items, items);
  return (
    <div className={"marquee " + className} aria-hidden="true">
      <div className="marquee-track">
        {run.map((it, i) => (
          <span key={i} className="marquee-item">
            {it}<span className="marquee-sep">{sep}</span>
          </span>
        ))}
      </div>
    </div>
  );
}

const Arw = () => <span className="arw" aria-hidden="true">→</span>;

/* true once the page is scrolled past a small threshold */
function useScrolled(threshold) {
  const [s, setS] = useState(false);
  useEffect(() => {
    const t = threshold || 24;
    const f = () => setS(window.scrollY > t);
    f();
    window.addEventListener("scroll", f, { passive: true });
    return () => window.removeEventListener("scroll", f);
  }, []);
  return s;
}

Object.assign(window, { useInView, Reveal, ClipLines, CountUp, Prose, Placeholder, Marquee, Arw, useScrolled });
