// motion.jsx - the site's motion engine. Scroll-position driven (IntersectionObserver
// is unreliable in some embedded preview frames), so everything is computed from
// getBoundingClientRect on scroll/resize/load:
//   - reveal-on-scroll for [.reveal] (CSS supplies the variant: rise/scale/blur/clip/left/right)
//   - nav background on scroll + a thin accent scroll-progress bar
//   - parallax drift for [data-parallax] (value = speed, +down/-up)
//   - scroll-linked transforms: [data-scroll-scale] / [data-scroll-rise] map the
//     element's progress through the viewport to a transform (choreographed entrances)
//   - pointer tilt [data-tilt], magnetic pull [data-magnetic], cursor spotlight [data-spotlight]
//   - scrollytelling: [data-scrolly] drives .scrolly-screen[data-step] by nearest step
// Exposed as window.BxMotion.init() -> returns a cleanup fn.
(function () {
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  const canHover = window.matchMedia("(hover: hover)").matches;
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const lerp = (a, b, t) => a + (b - a) * t;

  function init() {
    const cleanups = [];
    const big = window.__bxBigMotion !== false;

    // --- Transition-support probe ------------------------------------------
    // In some rendering environments (capture harnesses, throttled/background
    // tabs) CSS opacity/transform transitions never advance, which would leave
    // every .reveal stuck at opacity:0. Probe a short transition; if it does not
    // progress, switch the whole page to a static-but-visible mode so content
    // is NEVER hidden. Real browsers pass the probe and keep the full motion.
    (function probeTransitions() {
      const root = document.documentElement;
      if (reduced) { root.classList.add("motion-off"); return; }
      try {
        const p = document.createElement("div");
        p.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:2px;height:2px;opacity:0.01;transition:opacity 60ms linear;pointer-events:none;";
        document.body.appendChild(p);
        void p.offsetWidth;
        p.style.opacity = "1";
        setTimeout(() => {
          const moved = parseFloat(getComputedStyle(p).opacity) > 0.4;
          if (!moved) root.classList.add("motion-off");
          p.remove();
        }, 110);
      } catch (e) {
        root.classList.add("motion-off");
      }
    })();

    const revealEls = Array.from(document.querySelectorAll(".reveal"));
    const nav = document.querySelector(".nav");
    const parallaxEls = Array.from(document.querySelectorAll("[data-parallax]"));
    const scaleEls = Array.from(document.querySelectorAll("[data-scroll-scale]"));
    const riseEls = Array.from(document.querySelectorAll("[data-scroll-rise]"));
    const scrollies = Array.from(document.querySelectorAll("[data-scrolly]")).map((c) => ({
      container: c,
      steps: Array.from(c.querySelectorAll("[data-step]")),
      screens: Array.from(c.querySelectorAll(".scrolly-screen")),
    }));

    // progress bar
    let bar = document.querySelector(".scroll-progress");
    if (!bar) {
      bar = document.createElement("div");
      bar.className = "scroll-progress";
      bar.innerHTML = '<span></span>';
      document.body.appendChild(bar);
    }
    const barFill = bar.firstChild;

    if (!big || reduced) {
      [...parallaxEls, ...scaleEls, ...riseEls].forEach((el) => { el.style.transform = ""; });
      document.querySelectorAll("[data-tilt],[data-magnetic]").forEach((el) => { el.style.transform = ""; });
    }

    // ---- Scale the pinned ritual phone to always fit the viewport ----------
    // The app UI inside is laid out for a 300px-wide screen, so we scale the
    // whole device (not its width) - nothing inside ever clips. ~64px reserved
    // for the nav at top + a little breathing room top & bottom.
    const ritualPhone = document.querySelector("[data-ritual-phone]");
    function sizeRitualPhone() {
      if (!ritualPhone) return;
      const designH = 642; // phone height incl. bezel padding
      const avail = window.innerHeight - 96; // nav + margins
      const scale = Math.max(0.62, Math.min(1, avail / designH));
      ritualPhone.style.transform = scale < 1 ? `scale(${scale.toFixed(3)})` : "";
    }
    sizeRitualPhone();

    let ticking = false;
    function frame() {
      const vh = window.innerHeight;
      const y = window.scrollY || window.pageYOffset;
      const docH = document.documentElement.scrollHeight - vh;

      if (nav) nav.classList.toggle("scrolled", y > 24);
      if (barFill) barFill.style.transform = `scaleX(${docH > 0 ? clamp(y / docH, 0, 1) : 0})`;

      // reveals - trip when the element's top crosses 88% of the viewport
      for (let i = revealEls.length - 1; i >= 0; i--) {
        const el = revealEls[i];
        if (el.getBoundingClientRect().top < vh * 0.9) {
          el.classList.add("in");
          revealEls.splice(i, 1);
        }
      }

      if (!reduced && big) {
        // parallax drift
        parallaxEls.forEach((el) => {
          const speed = parseFloat(el.getAttribute("data-parallax")) || 0;
          const rect = el.getBoundingClientRect();
          const fromCenter = rect.top + rect.height / 2 - vh / 2;
          el.style.transform = `translate3d(0, ${(-fromCenter * speed).toFixed(1)}px, 0)`;
        });
        // scroll-linked scale (element grows toward 1 as it reaches centre)
        scaleEls.forEach((el) => {
          const from = parseFloat(el.getAttribute("data-scroll-scale")) || 0.9;
          const rect = el.getBoundingClientRect();
          // progress 0 when top at bottom of viewport, 1 when centred
          const p = clamp(1 - (rect.top + rect.height * 0.2 - vh * 0.5) / (vh * 0.6), 0, 1);
          el.style.transform = `scale(${lerp(from, 1, p).toFixed(4)})`;
        });
        // scroll-linked rise
        riseEls.forEach((el) => {
          const dist = parseFloat(el.getAttribute("data-scroll-rise")) || 60;
          const rect = el.getBoundingClientRect();
          const p = clamp(1 - (rect.top - vh * 0.5) / (vh * 0.5), 0, 1);
          el.style.transform = `translate3d(0, ${(lerp(dist, 0, p)).toFixed(1)}px, 0)`;
        });
      }

      // scrollytelling - activate the step whose centre is nearest viewport centre
      scrollies.forEach((s) => {
        if (!s.steps.length) return;
        let best = 0, bestDist = Infinity;
        s.steps.forEach((st) => {
          const r = st.getBoundingClientRect();
          const dist = Math.abs(r.top + r.height / 2 - vh / 2);
          if (dist < bestDist) { bestDist = dist; best = +st.getAttribute("data-step"); }
        });
        s.screens.forEach((sc) => sc.classList.toggle("show", +sc.getAttribute("data-step") === best));
        s.steps.forEach((st) => st.classList.toggle("active", +st.getAttribute("data-step") === best));
      });

      ticking = false;
    }
    function onScroll() {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(frame);
    }
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    window.addEventListener("resize", sizeRitualPhone, { passive: true });
    frame();
    [80, 250, 600].forEach((t) => setTimeout(() => requestAnimationFrame(frame), t));
    // Safety net: if reveals are still computed-hidden a moment after load (a
    // frozen-transition environment the probe somehow missed), force the whole
    // page into static-visible mode so nothing is ever lost.
    const safety = setTimeout(() => {
      const sample = document.querySelector(".reveal.in");
      if (sample && parseFloat(getComputedStyle(sample).opacity) < 0.5) {
        document.documentElement.classList.add("motion-off");
      }
      revealEls.forEach((el) => el.classList.add("in"));
      revealEls.length = 0;
    }, 2200);
    cleanups.push(() => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      window.removeEventListener("resize", sizeRitualPhone);
      clearTimeout(safety);
    });

    // ---- Pointer tilt ------------------------------------------------------
    if (!reduced && big && canHover) {
      Array.from(document.querySelectorAll("[data-tilt]")).forEach((el) => {
        const strength = parseFloat(el.getAttribute("data-tilt")) || 6;
        const parent = el.closest("[data-tilt-area]") || el.parentElement;
        function move(e) {
          const r = parent.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(1200px) rotateY(${(px * strength).toFixed(2)}deg) rotateX(${(-py * strength).toFixed(2)}deg)`;
        }
        function leave() { el.style.transform = "perspective(1200px) rotateY(0) rotateX(0)"; }
        parent.addEventListener("mousemove", move);
        parent.addEventListener("mouseleave", leave);
        cleanups.push(() => { parent.removeEventListener("mousemove", move); parent.removeEventListener("mouseleave", leave); });
      });

      // ---- Magnetic pull (buttons, social icons) --------------------------
      Array.from(document.querySelectorAll("[data-magnetic]")).forEach((el) => {
        const pull = parseFloat(el.getAttribute("data-magnetic")) || 0.3;
        function move(e) {
          const r = el.getBoundingClientRect();
          const dx = e.clientX - (r.left + r.width / 2);
          const dy = e.clientY - (r.top + r.height / 2);
          el.style.transform = `translate(${(dx * pull).toFixed(1)}px, ${(dy * pull).toFixed(1)}px)`;
        }
        function leave() { el.style.transform = "translate(0,0)"; }
        el.addEventListener("mousemove", move);
        el.addEventListener("mouseleave", leave);
        cleanups.push(() => { el.removeEventListener("mousemove", move); el.removeEventListener("mouseleave", leave); });
      });

      // ---- Cursor spotlight (dark bands) ----------------------------------
      Array.from(document.querySelectorAll("[data-spotlight]")).forEach((el) => {
        function move(e) {
          const r = el.getBoundingClientRect();
          el.style.setProperty("--mx", ((e.clientX - r.left) / r.width * 100).toFixed(1) + "%");
          el.style.setProperty("--my", ((e.clientY - r.top) / r.height * 100).toFixed(1) + "%");
          el.classList.add("spot-on");
        }
        function leave() { el.classList.remove("spot-on"); }
        el.addEventListener("mousemove", move);
        el.addEventListener("mouseleave", leave);
        cleanups.push(() => { el.removeEventListener("mousemove", move); el.removeEventListener("mouseleave", leave); });
      });
    }

    return () => cleanups.forEach((fn) => fn());
  }

  window.BxMotion = { init, reduced };
})();
