{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-states",
  "title": "Text States",
  "description": "A status label that swaps its text in place, exiting up with blur and entering from below.",
  "files": [
    {
      "path": "components/spectrumui/text-states.tsx",
      "content": "/**\n * Spectrum UI — TextStates\n *\n * A status label that swaps its text in place: the old text exits up with a\n * touch of blur, the new one enters from below. The motion is the\n * \"Text states swap\" recipe from transitions.dev (Jakub Antalík) — its\n * namespaced `t-text-swap` CSS is embedded verbatim, including the\n * reduced-motion guard — and this component runs the recipe's three-phase\n * class sequence whenever the `text` prop changes. Drop it inside a button\n * for Save → Saving… → Saved.\n *\n * Dependencies: @/lib/utils\n *\n * @example\n * <TextStates text={saving ? \"Saving…\" : saved ? \"Saved\" : \"Save\"} />\n */\n\n'use client';\n\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport interface TextStatesProps {\n  /** Current text. Each change runs the exit/enter swap */\n  text: string;\n  /** Swap duration in ms (each phase). Default 150 */\n  duration?: number;\n  /** Travel distance in px. Default 4 */\n  translateY?: number;\n  /** Blur amount in px. Default 2 */\n  blur?: number;\n  className?: string;\n}\n\n// transitions.dev · text-states-swap · defaults match the live tuning\nconst CSS = `\n.t-text-swap{display:inline-block;transform:translateY(0);filter:blur(0);opacity:1;transition:transform var(--text-swap-dur) var(--text-swap-ease),filter var(--text-swap-dur) var(--text-swap-ease),opacity var(--text-swap-dur) var(--text-swap-ease);will-change:transform,filter,opacity}\n.t-text-swap.is-exit{transform:translateY(calc(var(--text-swap-translate-y) * -1));filter:blur(var(--text-swap-blur));opacity:0}\n.t-text-swap.is-enter-start{transform:translateY(var(--text-swap-translate-y));filter:blur(var(--text-swap-blur));opacity:0;transition:none}\n@media (prefers-reduced-motion: reduce){.t-text-swap{transition:none !important}}\n`;\n\nexport function TextStates({\n  text,\n  duration = 150,\n  translateY = 4,\n  blur = 2,\n  className,\n}: TextStatesProps) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const [shown, setShown] = React.useState(text);\n\n  // Phase 1: exit the old text, then commit the new text after one duration\n  React.useEffect(() => {\n    if (text === shown) return;\n    const el = ref.current;\n    if (!el) return;\n    el.classList.add('is-exit');\n    const timer = window.setTimeout(() => setShown(text), duration);\n    return () => window.clearTimeout(timer);\n  }, [text, shown, duration]);\n\n  // Phases 2–3: jump below without transition, force a reflow, then release\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || !el.classList.contains('is-exit')) return;\n    el.classList.remove('is-exit');\n    el.classList.add('is-enter-start');\n    void el.offsetHeight;\n    el.classList.remove('is-enter-start');\n  }, [shown]);\n\n  return (\n    <>\n      <style>{CSS}</style>\n      <span\n        ref={ref}\n        className={cn('t-text-swap', className)}\n        style={\n          {\n            '--text-swap-dur': `${duration}ms`,\n            '--text-swap-translate-y': `${translateY}px`,\n            '--text-swap-blur': `${blur}px`,\n            '--text-swap-ease': 'ease-in-out',\n          } as React.CSSProperties\n        }\n      >\n        {shown}\n      </span>\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/text-states.tsx"
    }
  ],
  "type": "registry:component"
}
