// 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)",
          // Transform springs (gentle overshoot = "alive"); opacity stays linear-ish
          // so it never flickers past 1. Act I: the whole app's reveals inherit this.
          transition:
            "opacity " + duration + "ms var(--ease-out) " + delay + "ms, transform " + duration + "ms var(--ease-spring) " + 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: cycles words with a per-letter falling cascade ----
  // On each switch the old word's letters drop away one by one while the new
  // word's letters fall into place from above (soft blur, ~38ms stagger), and
  // the container width eases to the new word's width so a centered headline
  // glides instead of jumping. `reserve` (default true) instead reserves the
  // longest word's width so nothing reflows at all. 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.
  const ROTATE_CSS_ID = "ltq-rotating-word-css";
  function ensureRotateCss() {
    if (document.getElementById(ROTATE_CSS_ID)) return;
    const s = document.createElement("style");
    s.id = ROTATE_CSS_ID;
    s.textContent =
      "@keyframes ltw-fall-in { from { opacity: 0; transform: translateY(-0.6em); filter: blur(6px); } to { opacity: 1; transform: none; filter: blur(0); } }" +
      "@keyframes ltw-fall-out { from { opacity: 1; transform: none; filter: blur(0); } to { opacity: 0; transform: translateY(0.6em); filter: blur(6px); } }";
    document.head.appendChild(s);
  }

  function RotatingWord(props) {
    const { words = [], interval = 2200, reserve = true, className = "", style = {} } = props;
    const reduced = usePrefersReducedMotion();
    const [state, setState] = useState({ i: 0, prev: -1, gen: 0 });
    const wordRef = useRef(null);
    const [w, setW] = useState(null); // measured current-word width → smooth glide
    useEffect(() => { ensureRotateCss(); }, []);
    useEffect(() => {
      if (reduced || words.length < 2) return;
      const id = setInterval(
        () => setState((s) => ({ i: (s.i + 1) % words.length, prev: s.i, gen: s.gen + 1 })),
        interval,
      );
      return () => clearInterval(id);
    }, [reduced, words, interval]);
    const word = String(words[reduced ? 0 : state.i] || "");
    const prevWord = !reduced && state.prev >= 0 ? String(words[state.prev] || "") : null;
    React.useLayoutEffect(() => {
      if (reserve || reduced) return;
      if (wordRef.current) setW(wordRef.current.offsetWidth);
    }, [word, reserve, reduced]);
    // one span per letter; `gen` in the key re-triggers the cascade each switch
    const letters = (text, anim, stagger) =>
      Array.from(text).map((ch, idx) =>
        React.createElement(
          "span",
          {
            key: state.gen + "-" + idx,
            "aria-hidden": "true",
            style: {
              display: "inline-block",
              whiteSpace: "pre",
              animation: reduced ? "none" : anim + " both",
              animationDelay: idx * stagger + "ms",
            },
          },
          ch,
        ),
      );
    const currentLetters = letters(word, "ltw-fall-in 0.6s cubic-bezier(0.22, 1, 0.36, 1)", 38);
    const prevOverlay =
      prevWord === null
        ? null
        : React.createElement(
            "span",
            {
              "aria-hidden": "true",
              key: "prev-" + state.gen,
              style: reserve
                ? { position: "absolute", left: 0, right: 0, top: 0, whiteSpace: "nowrap", pointerEvents: "none" }
                : { position: "absolute", left: 0, top: 0, whiteSpace: "nowrap", pointerEvents: "none" },
            },
            letters(prevWord, "ltw-fall-out 0.42s cubic-bezier(0.55, 0, 0.55, 0.2)", 30),
          );
    if (!reserve) {
      return React.createElement(
        "span",
        {
          className,
          role: "text",
          "aria-label": word,
          style: {
            position: "relative", display: "inline-block", whiteSpace: "nowrap",
            width: w != null && !reduced ? w + "px" : undefined,
            transition: "width 0.5s cubic-bezier(0.22, 1, 0.36, 1)",
            ...style,
          },
        },
        React.createElement("span", { ref: wordRef, style: { display: "inline-block", whiteSpace: "pre" } }, currentLetters),
        prevOverlay,
      );
    }
    const longest = words.reduce((a, b) => (String(b).length > String(a).length ? b : a), "");
    return React.createElement(
      "span",
      { className, role: "text", "aria-label": word, 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: "cur", style: { position: "absolute", left: 0, right: 0, top: 0 } },
        currentLetters,
      ),
      prevOverlay,
    );
  }

  // ---- 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),
      ),
    );
  }

  // ---- Celebrate: restrained reward moment (coral ring + check draw + soft puff) ----
  // The premium-adapted version of Duolingo's completion beat: a check that DRAWS
  // itself, one small particle puff, then the whole thing fades out. Plays once on
  // mount (or when `play` flips true). Reduced-motion → a static check, no puff.
  const RING_LEN = 188; // 2πr, r=30
  const CHK_LEN = 46;
  // A small, tasteful brand palette for the celebration confetti.
  function celebrationColors() {
    const cs = getComputedStyle(document.documentElement);
    const g = (v, f) => (cs.getPropertyValue(v) || "").trim() || f;
    return [g("--accent-500", "#17a866"), g("--viz-blue", "#3b6ef5"), g("--viz-violet", "#7c5cf6"), g("--viz-amber", "#f2a52b"), g("--accent-500", "#17a866")];
  }
  // Real gravity physics: particles launch mostly upward with spread, gravity
  // pulls them down, they spin and fade — a proper confetti burst (rAF loop).
  function confettiBurst(wrap) {
    const colors = celebrationColors();
    const parts = [];
    for (let i = 0; i < 26; i++) {
      const el = document.createElement("span");
      const w = 5 + Math.random() * 5, h = 7 + Math.random() * 7;
      el.style.cssText = "position:absolute;left:50%;top:50%;width:" + w + "px;height:" + h + "px;border-radius:2px;pointer-events:none;will-change:transform,opacity;background:" + colors[i % colors.length] + ";";
      wrap.appendChild(el);
      const ang = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 1.15; // mostly up, wide spread
      const sp = 3 + Math.random() * 4.6;
      parts.push({ el, x: 0, y: 0, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, rot: Math.random() * 360, vr: (Math.random() - 0.5) * 26 });
    }
    const dur = 1150, start = performance.now();
    function frame(now) {
      const t = now - start; let alive = false;
      for (const p of parts) {
        if (p.dead) continue;
        p.vy += 0.22; p.x += p.vx; p.y += p.vy; p.rot += p.vr; // gravity + integrate
        const op = Math.max(0, 1 - t / dur);
        p.el.style.transform = "translate(calc(-50% + " + p.x + "px), calc(-50% + " + p.y + "px)) rotate(" + p.rot + "deg)";
        p.el.style.opacity = op;
        if (op <= 0) { p.dead = true; p.el.remove(); } else alive = true;
      }
      if (alive) requestAnimationFrame(frame); else parts.forEach((p) => { if (!p.dead) p.el.remove(); });
    }
    requestAnimationFrame(frame);
  }
  function radialFlash(wrap, col) {
    const f = document.createElement("span");
    f.style.cssText = "position:absolute;left:50%;top:50%;width:70%;height:70%;border-radius:999px;pointer-events:none;transform:translate(-50%,-50%);opacity:.5;background:radial-gradient(circle," + col + ",transparent 68%);";
    wrap.appendChild(f);
    f.animate([{ transform: "translate(-50%,-50%) scale(.35)", opacity: 0.5 }, { transform: "translate(-50%,-50%) scale(2.6)", opacity: 0 }], { duration: 640, easing: "cubic-bezier(.22,1,.36,1)", fill: "forwards" }).finished.then(function () { f.remove(); }).catch(function () {});
  }
  function Celebrate(props) {
    const { size = 72, color = "var(--accent-500)", play = true, onDone, style = {} } = props;
    const reduced = usePrefersReducedMotion();
    const wrapRef = useRef(null);
    const ringRef = useRef(null);
    const checkRef = useRef(null);
    useEffect(() => {
      if (!play) return;
      const wrap = wrapRef.current, ring = ringRef.current, chk = checkRef.current;
      if (!wrap || !ring) return;
      const done = () => { if (onDone) onDone(); };
      if (reduced || typeof ring.animate !== "function") { const id = setTimeout(done, 600); return () => clearTimeout(id); }
      const timers = [];
      ring.animate([{ strokeDashoffset: RING_LEN }, { strokeDashoffset: 0 }], { duration: 520, easing: "cubic-bezier(.22,1,.36,1)", fill: "forwards" });
      timers.push(setTimeout(() => {
        chk.animate([{ strokeDashoffset: CHK_LEN }, { strokeDashoffset: 0 }], { duration: 300, easing: "cubic-bezier(.34,1.56,.64,1)", fill: "forwards" });
        wrap.animate([{ transform: "scale(.7)" }, { transform: "scale(1.1)" }, { transform: "scale(1)" }], { duration: 520, easing: "cubic-bezier(.22,1.61,.36,1)" });
        // resolve the accent to a concrete color for the flash, then burst confetti
        const probe = document.createElement("span");
        probe.style.color = color; document.body.appendChild(probe);
        const col = getComputedStyle(probe).color; probe.remove();
        radialFlash(wrap, col);
        confettiBurst(wrap);
      }, 360));
      timers.push(setTimeout(() => {
        wrap.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 440, easing: "ease-out", fill: "forwards" }).finished.then(done).catch(done);
      }, 1500));
      return () => timers.forEach(clearTimeout);
    }, [play, reduced]);
    const off = reduced ? 0 : undefined; // reduced-motion: draw statically
    return React.createElement(
      "div",
      { ref: wrapRef, "aria-hidden": "true", style: { position: "relative", width: size, height: size, ...style } },
      React.createElement(
        "svg",
        { width: size, height: size, viewBox: "0 0 66 66", fill: "none" },
        React.createElement("circle", { ref: ringRef, cx: 33, cy: 33, r: 30, stroke: color, "stroke-width": 3, "stroke-linecap": "round", "stroke-dasharray": RING_LEN, "stroke-dashoffset": off != null ? off : RING_LEN }),
        React.createElement("path", { ref: checkRef, d: "M20 34l9 9 18-20", stroke: color, "stroke-width": 4.5, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-dasharray": CHK_LEN, "stroke-dashoffset": off != null ? off : CHK_LEN }),
      ),
    );
  }

  // ---- Odometer: per-digit rolling counter (Duolingo XP-counter style) ----
  // Each digit is a 0-9 vertical strip clipped to one glyph; on scroll-into-view the
  // strip slides to the target digit on a spring, staggered left→right. Tabular so
  // columns never jitter. Reduced-motion → the number renders statically.
  function Odometer(props) {
    const { value = 0, duration = 950, className = "", style = {} } = props;
    const reduced = usePrefersReducedMotion();
    const [ref, inView] = useInView({ once: true });
    const chars = Math.round(Number(value) || 0).toLocaleString().split("");
    return React.createElement(
      "span",
      { ref, className, style: { display: "inline-flex", fontVariantNumeric: "tabular-nums", ...style } },
      chars.map((c, i) => {
        if (!/[0-9]/.test(c)) return React.createElement("span", { key: "s" + i }, c); // "," etc.
        const n = parseInt(c, 10);
        const target = reduced ? n : inView ? n : 0;
        return React.createElement(
          "span",
          { key: i, style: { display: "inline-block", height: "1em", lineHeight: "1em", overflow: "hidden", verticalAlign: "bottom" } },
          React.createElement(
            "span",
            {
              style: {
                display: "block",
                transform: "translateY(-" + target + "em)",
                transition: reduced ? "none" : "transform " + duration + "ms cubic-bezier(.2,1.05,.36,1) " + i * 55 + "ms",
                willChange: "transform",
              },
            },
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((k) =>
              React.createElement("span", { key: k, style: { display: "block", height: "1em", lineHeight: "1em" } }, k),
            ),
          ),
        );
      }),
    );
  }

  const api = { usePrefersReducedMotion, useInView, Reveal, RevealGroup, Marquee, Counter, Odometer, RotatingWord, PulseDot, Tilt, Accordion, Celebrate };
  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");
})();
