"use client";

// components/ui/TextSwap.tsx
// The Transitions.dev "text states swap" (recipe 04): when the text changes, the
// old label exits up with blur, the new one enters from below. Used for in-place
// button-label changes ("Copiar mensaje" -> "Mensaje copiado", "Continuar" ->
// "Inscribiendo...", the pay total).
//
// React adaptation of the recipe's imperative orchestration: the swap phase is
// driven by React state (so React, not imperative classList, owns the className
// and there is no re-render race), and React owns the displayed text. Every state
// update happens inside an async callback (rAF / timeout), never synchronously in
// the effect body, so it does not trip the set-state-in-effect rule.
//
// SSR-safe: the initial label renders directly (display === children on first
// paint). prefers-reduced-motion: the text changes instantly, no animation.

import * as React from "react";

type Phase = "idle" | "exit" | "enter";

function usePrefersReducedMotion(): boolean {
  const [reduced, setReduced] = React.useState(false);
  React.useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(mq.matches);
    update();
    mq.addEventListener?.("change", update);
    return () => mq.removeEventListener?.("change", update);
  }, []);
  return reduced;
}

function readSwapDurationMs(): number {
  if (typeof window === "undefined") return 150;
  const raw = getComputedStyle(document.documentElement).getPropertyValue(
    "--text-swap-dur",
  );
  const n = parseFloat(raw);
  return Number.isFinite(n) ? n : 150;
}

export interface TextSwapProps {
  /** The current label. Changing it triggers the swap. */
  children: string;
  className?: string;
}

export function TextSwap({ children, className }: TextSwapProps) {
  const reduced = usePrefersReducedMotion();
  const [display, setDisplay] = React.useState(children);
  const [phase, setPhase] = React.useState<Phase>("idle");
  const prevRef = React.useRef(children);

  React.useEffect(() => {
    if (children === prevRef.current) return;
    prevRef.current = children;

    let raf0 = 0;
    let raf1 = 0;
    let raf2 = 0;
    let timer = 0;

    if (reduced) {
      // No animation: swap the text on the next frame (keeps the setState out of
      // the synchronous effect body).
      raf0 = window.requestAnimationFrame(() => setDisplay(children));
      return () => window.cancelAnimationFrame(raf0);
    }

    // Phase 1: exit the old label (started on the next frame, not synchronously).
    raf0 = window.requestAnimationFrame(() => {
      setPhase("exit");
      // Phase 2 (after the exit duration): show the new label jumped below with
      // no transition, then Phase 3 (next frame): release to animate to rest.
      timer = window.setTimeout(() => {
        setDisplay(children);
        setPhase("enter");
        raf1 = window.requestAnimationFrame(() => {
          raf2 = window.requestAnimationFrame(() => setPhase("idle"));
        });
      }, readSwapDurationMs());
    });

    return () => {
      window.cancelAnimationFrame(raf0);
      window.cancelAnimationFrame(raf1);
      window.cancelAnimationFrame(raf2);
      window.clearTimeout(timer);
    };
  }, [children, reduced]);

  const phaseClass =
    phase === "exit" ? "is-exit" : phase === "enter" ? "is-enter-start" : "";
  const cls = ["t-text-swap", phaseClass, className].filter(Boolean).join(" ");

  return <span className={cls}>{display}</span>;
}
