{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-texts",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "morph-texts.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useEffect, useId, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MORPH_EASE = [0.42, 0, 0.58, 1] as const;\nconst MORPH_TRANSITION_DURATION = 0.9;\n\nconst wordVariants = {\n  initial: {\n    opacity: 0,\n    filter: \"blur(20px)\",\n    scale: 0.8,\n  },\n  animate: {\n    opacity: 1,\n    filter: \"blur(0px)\",\n    scale: 1,\n  },\n  exit: {\n    opacity: 0,\n    filter: \"blur(20px)\",\n    scale: 1.2,\n  },\n};\n\nexport interface MorphTextProps {\n  /** Array of words / phrases to cycle through. */\n  words: string[];\n  /**\n   * Duration (ms) each word is displayed before transitioning.\n   * @default 3000\n   */\n  interval?: number;\n  /**\n   * Optional subtext rendered beneath the morphing word.\n   */\n  subtext?: string;\n  /**\n   * Font size passed as a CSS value (e.g. \"clamp(3rem, 15vw, 10rem)\").\n   * Defaults to a fluid clamp that scales with the viewport.\n   */\n  fontSize?: string;\n  /**\n   * Font family. Defaults to `\"Space Grotesk\", sans-serif`.\n   */\n  fontFamily?: string;\n  /** Extra CSS classes on the root wrapper. */\n  className?: string;\n  /** Extra CSS classes on the morphing text container. */\n  textClassName?: string;\n  /** Extra CSS classes on the subtext element. */\n  subtextClassName?: string;\n}\n\nexport function MorphText({\n  words,\n  interval = 3000,\n  subtext,\n  fontSize = \"clamp(3rem, 15vw, 10rem)\",\n  fontFamily = '\"Space Grotesk\", sans-serif',\n  className,\n  textClassName,\n  subtextClassName,\n}: MorphTextProps) {\n  const [activeIndex, setActiveIndex] = useState(0);\n  const uid = useId().replace(/:/g, \"\");\n  const filterId = `morph-threshold-${uid}`;\n  const activeWord = words[activeIndex] ?? words[0];\n\n  useEffect(() => {\n    if (words.length <= 1) {\n      return;\n    }\n\n    const timer = setInterval(() => {\n      setActiveIndex((current) => (current + 1) % words.length);\n    }, interval);\n\n    return () => clearInterval(timer);\n  }, [interval, words.length]);\n\n  const wordTransition = {\n    duration: MORPH_TRANSITION_DURATION,\n    ease: MORPH_EASE,\n  };\n\n  if (words.length === 0) {\n    return null;\n  }\n\n  const longestWord = words.reduce(\n    (longest, word) => (word.length > longest.length ? word : longest),\n    words[0]\n  );\n\n  const morphWord = (\n    <span\n      className={cn(\n        \"morph-text-container relative inline-grid align-baseline leading-none\",\n        textClassName\n      )}\n      style={{\n        fontSize,\n        filter: `url(#${filterId})`,\n        fontFamily,\n        verticalAlign: \"baseline\",\n      }}\n    >\n      <span\n        aria-hidden\n        className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n      >\n        {longestWord}\n      </span>\n      <span\n        aria-live=\"polite\"\n        className=\"relative col-start-1 row-start-1 whitespace-nowrap\"\n      >\n        <AnimatePresence>\n          <motion.span\n            animate=\"animate\"\n            className=\"morph-word absolute top-0 left-0 whitespace-nowrap\"\n            exit=\"exit\"\n            initial=\"initial\"\n            key={`${activeWord}-${activeIndex}`}\n            style={{ transformOrigin: \"0% 100%\" }}\n            transition={wordTransition}\n            variants={wordVariants}\n          >\n            {activeWord}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n\n  if (subtext) {\n    return (\n      <div\n        className={cn(\n          \"morph-text-root relative flex flex-col items-center\",\n          className\n        )}\n      >\n        <MorphTextFilter filterId={filterId} />\n        {morphWord}\n        <motion.p\n          animate={{ opacity: 1, y: 0 }}\n          className={cn(\n            \"morph-subtext mt-8 text-[#888] uppercase tracking-[0.2em]\",\n            subtextClassName\n          )}\n          initial={{ opacity: 0, y: 20 }}\n          style={{\n            fontSize: \"1.2rem\",\n            fontFamily,\n          }}\n          transition={{ duration: 1, ease: \"easeOut\", delay: 1 }}\n        >\n          {subtext}\n        </motion.p>\n      </div>\n    );\n  }\n\n  return (\n    <span\n      className={cn(\n        \"morph-text-root relative inline-grid align-baseline leading-none\",\n        className\n      )}\n      style={{ verticalAlign: \"baseline\" }}\n    >\n      <MorphTextFilter filterId={filterId} />\n      {morphWord}\n    </span>\n  );\n}\n\nfunction MorphTextFilter({ filterId }: { filterId: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      style={{\n        position: \"absolute\",\n        width: 0,\n        height: 0,\n        pointerEvents: \"none\",\n      }}\n    >\n      <defs>\n        <filter id={filterId}>\n          <feColorMatrix\n            in=\"SourceGraphic\"\n            result=\"goo\"\n            type=\"matrix\"\n            values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 25 -9\"\n          />\n          <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n        </filter>\n      </defs>\n    </svg>\n  );\n}\n\nexport default MorphText;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Morph Text",
  "description": "Cycling headline words with a goo-filter morph transition powered by Motion."
}
