// SQSEO shared motion helpers (Phase 2 of the BLG-inspired redesign).
// React components that DRIVE the CSS animation layer in tokens/anim.css:
//   Reveal       — scroll-into-view entrance (IntersectionObserver), staggerable
//   Marquee      — seamless infinite logo/chip strip (duplicated track, pause-on-hover)
//   Counter      — count-up to a number when it scrolls into view
//   RotatingWord — cycles words with the fx-swap micro-animation (hero word)
//   PulseDot     — "live" indicator (pulse-wave ring + solid core)
//   useInView, usePrefersReducedMotion — shared hooks
// Framework-agnostic within this no-build setup: registers on a neutral window.LTQFX
// and mirrors into BOTH window.LTQM (marketing) and window.LTQ (app), so any page can
// use it. Pure React (no design-system dependency). Everything degrades gracefully and
// respects prefers-reduced-motion.
(function init() {
  if (!window.React) return setTimeout(init, 30);
  const React = window.React;
  const { useState, useEffect, useRef, useCallback } = React;

  // ---- reduced-motion (live) ----
  function usePrefersReducedMotion() {
    const get = () =>
      typeof window.matchMedia === "function" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const [reduced, setReduced] = useState(get);
    useEffect(() => {
      if (typeof window.matchMedia !== "function") return;
      const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
      const on = () => setReduced(mq.matches);
      mq.addEventListener ? mq.addEventListener("change", on) : mq.addListener(on);
      return () => (mq.removeEventListener ? mq.removeEventListener("change", on) : mq.removeListener(on));
    }, []);
    return reduced;
  }

  // ---- in-view observer (returns [ref, inView]) ----
  function useInView(opts) {
    opts = opts || {};
    const { rootMargin = "0px 0px -10% 0px", threshold = 0.15, once = true } = opts;
    const ref = useRef(null);
    const [inView, setInView] = useState(false);
    useEffect(() => {
      const el = ref.current;
      if (!el) return;
      if (typeof IntersectionObserver === "undefined") {
        setInView(true); // no observer support → just show
        return;
      }
      const io = new IntersectionObserver(
        (entries) => {
          for (const e of entries) {
            if (e.isIntersecting) {
              setInView(true);
              if (once) io.disconnect();
            } else if (!once) {
              setInView(false);
            }
          }
        },
        { rootMargin, threshold },
      );
      io.observe(el);
      return () => io.disconnect();
    }, [rootMargin, threshold, once]);
    return [ref, inView];
  }

  // ---- Reveal: fade + rise into view ----
  function Reveal(props) {
    const {
      as = "div",
      children,
      delay = 0,
      y = 16,
      duration = 700,
      once = true,
      className = "",
      style = {},
      ...rest
    } = props;
    const reduced = usePrefersReducedMotion();
    const [ref, inView] = useInView({ once });
    const shown = reduced || inView;
    const motionStyle = reduced
      ? {}
      : {
          opacity: shown ? 1 : 0,
          transform: shown ? "none" : "translateY(" + y + "px)",
          transition:
            "opacity " + duration + "ms var(--ease-out) " + delay + "ms, transform " + duration + "ms var(--ease-out) " + delay + "ms",
          willChange: "opacity, transform",
        };
    return React.createElement(as, { ref, className, style: { ...motionStyle, ...style }, ...rest }, children);
  }

  // ---- Marquee: seamless infinite strip ----
  // Renders the items twice as direct children of the track (uniform gap) so the
  // -50% translate loops with no jump. The second pass is aria-hidden via re-keying
  // and a wrapper is avoided to keep the gap math exact.
  function Marquee(props) {
    const { children, speed = 26, direction = "left", pauseOnHover = true, fade = true, gap, className = "", style = {} } = props;
    const trackStyle = {
      animationDuration: speed + "s",
      animationDirection: direction === "right" ? "reverse" : "normal",
    };
    if (gap != null) trackStyle.gap = typeof gap === "number" ? gap + "px" : gap;
    const arr = React.Children.toArray(children);
    const rekey = (pass) =>
      arr.map((c, idx) => (React.isValidElement(c) ? React.cloneElement(c, { key: pass + idx }) : c));
    const cls = "fx-marquee " + (pauseOnHover ? "" : "fx-marquee-nopause ") + className;
    const mask = fade ? {} : { WebkitMask: "none", mask: "none" };
    return React.createElement(
      "div",
      { className: cls.trim(), style: { ...mask, ...style } },
      React.createElement("div", { className: "fx-marquee-track", style: trackStyle }, ...rekey("a"), ...rekey("b")),
    );
  }

  // ---- Counter: count-up when in view ----
  function Counter(props) {
    const {
      to = 0,
      from = 0,
      duration = 1200,
      decimals = 0,
      prefix = "",
      suffix = "",
      locale = true,
      className = "",
      style = {},
    } = props;
    const reduced = usePrefersReducedMotion();
    const [ref, inView] = useInView({ once: true });
    const [val, setVal] = useState(from);
    const raf = useRef(0);
    useEffect(() => {
      if (!inView) return;
      if (reduced || duration <= 0) {
        setVal(to);
        return;
      }
      const start = performance.now();
      const ease = (t) => 1 - Math.pow(1 - t, 3); // easeOutCubic
      const step = (now) => {
        const p = Math.min(1, (now - start) / duration);
        setVal(from + (to - from) * ease(p));
        if (p < 1) raf.current = requestAnimationFrame(step);
      };
      raf.current = requestAnimationFrame(step);
      return () => cancelAnimationFrame(raf.current);
    }, [inView, reduced, to, from, duration]);
    const rounded = decimals > 0 ? Number(val.toFixed(decimals)) : Math.round(val);
    const text = locale ? rounded.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : String(rounded);
    return React.createElement("span", { ref, className, style }, prefix + text + suffix);
  }

  // ---- RotatingWord: cycle words with the fx-swap micro-animation ----
  // `reserve` (default true) reserves the width of the longest word with an
  // invisible sizer and overlays the current word, so a centered headline never
  // reflows/jumps as the word changes. Under prefers-reduced-motion it does NOT
  // auto-cycle (a word changing every couple of seconds is a vestibular concern) —
  // it shows the first word statically.
  function RotatingWord(props) {
    const { words = [], interval = 2200, reserve = true, className = "", style = {} } = props;
    const reduced = usePrefersReducedMotion();
    const [i, setI] = useState(0);
    useEffect(() => {
      if (reduced || words.length < 2) return;
      const id = setInterval(() => setI((x) => (x + 1) % words.length), interval);
      return () => clearInterval(id);
    }, [reduced, words, interval]);
    const word = words[reduced ? 0 : i] || "";
    if (!reserve) {
      return React.createElement(
        "span",
        { key: i, className: (reduced ? "" : "fx-swap ") + className, style: { display: "inline-block", whiteSpace: "nowrap", ...style } },
        word,
      );
    }
    const longest = words.reduce((a, b) => (String(b).length > String(a).length ? b : a), "");
    return React.createElement(
      "span",
      { className, style: { position: "relative", display: "inline-block", whiteSpace: "nowrap", textAlign: "center", ...style } },
      React.createElement("span", { "aria-hidden": "true", style: { visibility: "hidden" } }, longest),
      React.createElement(
        "span",
        { key: reduced ? "static" : i, className: reduced ? "" : "fx-swap", style: { position: "absolute", left: 0, right: 0, top: 0 } },
        word,
      ),
    );
  }

  // ---- PulseDot: "live" indicator ----
  function PulseDot(props) {
    const { size = 8, color = "var(--accent-500)", className = "", style = {} } = props;
    const wrap = { position: "relative", display: "inline-block", width: size, height: size, ...style };
    const ring = { position: "absolute", inset: 0, borderRadius: "999px", background: color, opacity: 0.45 };
    const core = { position: "absolute", inset: 0, borderRadius: "999px", background: color };
    return React.createElement(
      "span",
      { className: "fx-livedot " + className, style: wrap, "aria-hidden": "true" },
      React.createElement("span", { className: "fx-pulse-wave", style: ring }),
      React.createElement("span", { style: core }),
    );
  }

  // ---- RevealGroup: stagger a set of children into view ----
  function RevealGroup(props) {
    const { children, stagger = 90, y = 16, duration = 700, as = "div", className = "", style = {}, childStyle = {} } = props;
    const arr = React.Children.toArray(children);
    return React.createElement(
      as,
      { className, style },
      arr.map((c, i) => React.createElement(Reveal, { key: i, delay: i * stagger, y, duration, style: childStyle }, c)),
    );
  }

  // ---- Tilt: 3D pointer tilt with a cursor-following glare (reduced-motion safe) ----
  function Tilt(props) {
    const { children, max = 7, scale = 1, glare = true, className = "", style = {} } = props;
    const reduced = usePrefersReducedMotion();
    const ref = useRef(null);
    const glareRef = useRef(null);
    const onMove = (e) => {
      if (reduced) return;
      const el = ref.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const px = (e.clientX - r.left) / r.width - 0.5;
      const py = (e.clientY - r.top) / r.height - 0.5;
      el.style.transform =
        "perspective(900px) rotateX(" + (-py * max).toFixed(2) + "deg) rotateY(" + (px * max).toFixed(2) + "deg) scale(" + scale + ")";
      if (glareRef.current) {
        glareRef.current.style.opacity = "1";
        glareRef.current.style.background =
          "radial-gradient(280px circle at " + (e.clientX - r.left) + "px " + (e.clientY - r.top) + "px, rgba(255,255,255,0.18), transparent 60%)";
      }
    };
    const onLeave = () => {
      if (ref.current) ref.current.style.transform = "perspective(900px) rotateX(0) rotateY(0) scale(1)";
      if (glareRef.current) glareRef.current.style.opacity = "0";
    };
    return React.createElement(
      "div",
      { ref, className, onMouseMove: onMove, onMouseLeave: onLeave, style: { position: "relative", transition: "transform 0.45s var(--ease-out)", transformStyle: "preserve-3d", ...style } },
      children,
      glare ? React.createElement("div", { ref: glareRef, "aria-hidden": "true", style: { position: "absolute", inset: 0, borderRadius: "inherit", pointerEvents: "none", opacity: 0, transition: "opacity 0.3s var(--ease-out)", zIndex: 5 } }) : null,
    );
  }

  // ---- Accordion: accessible disclosure list with smooth height (.fx-acc) ----
  function AccordionItem(props) {
    const { q, children, open, onToggle, idx } = props;
    const base = "lt-acc-" + idx;
    return React.createElement(
      "div",
      { style: { borderBottom: "1px solid var(--border-subtle)" } },
      React.createElement(
        "button",
        {
          id: base + "-btn",
          "aria-expanded": open ? "true" : "false",
          "aria-controls": base + "-panel",
          onClick: onToggle,
          style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, width: "100%", padding: "16px 4px", background: "transparent", border: "none", cursor: "pointer", textAlign: "left", fontFamily: "var(--font-sans)", fontSize: 15, fontWeight: 600, color: "var(--text-strong)" },
        },
        React.createElement("span", null, q),
        React.createElement(
          "span",
          { "aria-hidden": "true", style: { flex: "none", fontSize: 22, lineHeight: 1, color: "var(--text-faint)", transition: "transform var(--dur-base) var(--ease-out)", transform: open ? "rotate(45deg)" : "none" } },
          "+",
        ),
      ),
      React.createElement(
        "div",
        { id: base + "-panel", role: "region", "aria-labelledby": base + "-btn", className: "fx-acc" + (open ? " open" : "") },
        React.createElement(
          "div",
          null,
          React.createElement("div", { style: { padding: "0 4px 18px", color: "var(--text-muted)", fontSize: 14, lineHeight: 1.6 } }, children),
        ),
      ),
    );
  }
  function Accordion(props) {
    const { items = [], multiple = false, defaultOpen = [], className = "", style = {} } = props;
    const [open, setOpen] = useState(() => new Set(defaultOpen));
    const toggle = (i) =>
      setOpen((s) => {
        const n = new Set(multiple ? s : []);
        n.has(i) ? n.delete(i) : n.add(i);
        return n;
      });
    return React.createElement(
      "div",
      { className: "lt-acc " + className, style },
      items.map((it, i) =>
        React.createElement(AccordionItem, { key: i, idx: i, q: it.q, open: open.has(i), onToggle: () => toggle(i) }, it.a),
      ),
    );
  }

  const api = { usePrefersReducedMotion, useInView, Reveal, RevealGroup, Marquee, Counter, RotatingWord, PulseDot, Tilt, Accordion };
  window.LTQFX = api;
  // Non-destructive: never clobber a name an earlier module (e.g. marketing/fx.jsx's
  // LTQM.Tilt) already defined; just fill in what's missing on each namespace.
  const put = (ns) => {
    window[ns] = window[ns] || {};
    for (const k in api) if (!(k in window[ns])) window[ns][k] = api[k];
  };
  put("LTQM");
  put("LTQ");
})();
