{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-text",
  "type": "registry:component",
  "title": "Morphing text",
  "description": "A headline that cycles through phrases by melting each one into the next with a blur and crossfade on two stacked layers.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/morphing-text.tsx",
      "type": "registry:ui",
      "target": "components/ui/morphing-text.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { animate, motion, useInView, useMotionTemplate, useMotionValue, useReducedMotion, useTransform, type AnimationPlaybackControls } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * A headline that cycles through phrases by melting each one into the next with a blur and crossfade on two stacked layers.\n * The old words blur and spread like wet ink while the new ones condense\n * into focus. It holds the tallest phrase's height so nothing below it\n * jumps, pauses off screen, and shows the first phrase for reduced motion.\n * Screen readers get the whole list once.\n */\ntype MorphingTextProps = {\n  words?: string[]\n  /** Milliseconds each phrase is held before it melts into the next. */\n  interval?: number\n  /** Seconds one morph takes. */\n  duration?: number\n  className?: string\n}\n\nconst EASE: [number, number, number, number] = [0.65, 0, 0.35, 1]\n\nfunction MorphingText({\n  words = [\"Invoices that close themselves\", \"Reports that write themselves\", \"Reminders that send themselves\", \"Books that balance themselves\"],\n  interval = 2600,\n  duration = 1,\n  className,\n}: MorphingTextProps) {\n  const reduce = useReducedMotion()\n  const ref = React.useRef<HTMLParagraphElement>(null)\n  const inView = useInView(ref, { amount: 0.4 })\n  // Index of the phrase on screen. Phrases alternate between two stacked layers, so the hidden layer can be swapped without a flash.\n  const [front, setFront] = React.useState(0)\n  // 0 means layer A is in focus, 1 means layer B is. The morph animates between them.\n  const t = useMotionValue(0)\n  const n = Math.max(1, words.length)\n\n  const blurA = useTransform(t, [0, 1], [0, 14])\n  const blurB = useTransform(t, [0, 1], [14, 0])\n  const opacityA = useTransform(t, [0, 0.72], [1, 0])\n  const opacityB = useTransform(t, [0.28, 1], [0, 1])\n  // The leaving phrase spreads a touch as it blurs, the arriving one condenses, like ink drying.\n  const scaleA = useTransform(t, [0, 1], [1, 1.04])\n  const scaleB = useTransform(t, [0, 1], [0.96, 1])\n  const filterA = useMotionTemplate`blur(${blurA}px)`\n  const filterB = useMotionTemplate`blur(${blurB}px)`\n\n  React.useEffect(() => {\n    if (reduce || !inView || n < 2) return\n    let live = true\n    let controls: AnimationPlaybackControls | undefined\n    const target = front % 2 === 0 ? 1 : 0\n    // If we were paused mid morph, finish it now instead of holding a half melted word.\n    const wait = t.get() === 1 - target ? interval : 0\n    const id = window.setTimeout(() => {\n      controls = animate(t, target, {\n        duration,\n        ease: EASE,\n        onComplete: () => {\n          if (live) setFront((f) => f + 1)\n        },\n      })\n    }, wait)\n    return () => {\n      live = false\n      window.clearTimeout(id)\n      controls?.stop()\n    }\n  }, [front, inView, reduce, interval, duration, n, t])\n\n  const wordA = words[(front % 2 === 0 ? front : front + 1) % n] ?? \"\"\n  const wordB = words[(front % 2 === 1 ? front : front + 1) % n] ?? \"\"\n\n  return (\n    <p ref={ref} data-slot=\"morphing-text\" className={cn(\"mx-auto max-w-3xl text-center text-4xl font-medium tracking-[-0.045em] text-balance text-fg sm:text-5xl\", className)}>\n      <span className=\"sr-only\">{words.join(\". \")}</span>\n      <span aria-hidden className=\"inline-grid w-full\">\n        {/* Every phrase sits invisibly in the same cell, so the slot is always tall enough for the longest one at this width. */}\n        {words.map((w, i) => (\n          <span key={i} className=\"invisible col-start-1 row-start-1\">\n            {w}\n          </span>\n        ))}\n        {reduce ? (\n          <span className=\"col-start-1 row-start-1\">{words[0]}</span>\n        ) : (\n          <>\n            <motion.span className=\"col-start-1 row-start-1 will-change-[filter,opacity]\" style={{ filter: filterA, opacity: opacityA, scale: scaleA }}>\n              {wordA}\n            </motion.span>\n            <motion.span className=\"col-start-1 row-start-1 will-change-[filter,opacity]\" style={{ filter: filterB, opacity: opacityB, scale: scaleB }}>\n              {wordB}\n            </motion.span>\n          </>\n        )}\n      </span>\n    </p>\n  )\n}\n\nexport { MorphingText }\nexport type { MorphingTextProps }\n"
    }
  ]
}
