{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-morphing","title":"Text Morphing","author":"CoDesign-Spa27","description":"An animated text replacement primitive for status messages, hero copy, and compact state changes.","dependencies":["motion"],"files":[{"path":"components/ui-components/text-morphing.tsx","content":"\"use client\";\n\nimport { AnimatePresence, LayoutGroup, motion, type Transition, type Variants } from \"motion/react\";\nimport type { CSSProperties, ElementType } from \"react\";\nimport { useEffect, useId, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface MorphingTextProps {\n  texts: string[];\n  className?: string;\n  interval?: number;\n}\n\ntype TextMorphProps = {\n  children: string;\n  as?: ElementType;\n  className?: string;\n  style?: CSSProperties;\n  variants?: Variants;\n  transition?: Transition;\n  layoutTransition?: Transition;\n};\n\nconst defaultVariants: Variants = {\n  initial: {\n    opacity: 0,\n    filter: \"blur(0px)\",\n    y: 6,\n  },\n  animate: {\n    opacity: 1,\n    filter: \"blur(10px)\",\n    y: 0,\n  },\n  exit: {\n    opacity: 0,\n    filter: \"blur(0px)\",\n    y: -6,\n  },\n};\n\nconst defaultPresenceTransition: Transition = {\n  duration: 0.22,\n  ease: \"easeOut\",\n};\n\nconst defaultLayoutTransition: Transition = {\n  type: \"spring\",\n  bounce: 0.12,\n};\n\nconst getCharacterTokens = (text: string, uniqueId: string) => {\n  const characterCounts = new Map<string, number>();\n\n  return Array.from(text).map((character) => {\n    const normalizedCharacter = character.toLowerCase();\n    const occurrence = (characterCounts.get(normalizedCharacter) ?? 0) + 1;\n    characterCounts.set(normalizedCharacter, occurrence);\n\n    return {\n      id: `${uniqueId}-${normalizedCharacter}-${occurrence}`,\n      displayCharacter: character === \" \" ? \"\\u00A0\" : character,\n    };\n  });\n};\n\nexport const TextMorph = ({\n  children,\n  as: Component = \"span\",\n  className,\n  style,\n  variants = defaultVariants,\n  transition = defaultPresenceTransition,\n  layoutTransition = defaultLayoutTransition,\n}: TextMorphProps) => {\n  const uniqueId = useId();\n  const characters = useMemo(() => getCharacterTokens(children, uniqueId), [children, uniqueId]);\n  const animate =\n    variants === defaultVariants\n      ? {\n          opacity: 1,\n          filter: [\"blur(10px)\", \"blur(10px)\", \"blur(0px)\"],\n          y: 0,\n        }\n      : \"animate\";\n\n  return (\n    <Component\n      aria-label={children}\n      className={cn(\"relative isolate overflow-hidden whitespace-pre leading-none\", className)}\n      style={style}\n    >\n      <LayoutGroup id={uniqueId}>\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {characters.map((character) => (\n            <motion.span\n              aria-hidden=\"true\"\n              className=\"inline-block\"\n              initial=\"initial\"\n              animate={animate}\n              exit=\"exit\"\n              key={character.id}\n              layout\n              layoutId={character.id}\n              variants={variants}\n              transition={{\n                ...transition,\n                filter: {\n                  duration: 0.22,\n                  ease: \"easeOut\",\n                },\n                opacity: {\n                  duration: 0.22,\n                  ease: \"easeOut\",\n                },\n                y: {\n                  duration: 0.22,\n                  ease: \"easeOut\",\n                },\n                layout: layoutTransition,\n              }}\n            >\n              {character.displayCharacter}\n            </motion.span>\n          ))}\n        </AnimatePresence>\n      </LayoutGroup>\n    </Component>\n  );\n};\n\nexport const MorphingText = ({ texts, className, interval = 2000 }: MorphingTextProps) => {\n  const [index, setIndex] = useState(0);\n  const currentText = texts[index % texts.length] ?? \"\";\n\n  useEffect(() => {\n    if (texts.length < 2) return;\n\n    const timer = window.setInterval(() => {\n      setIndex((currentIndex) => (currentIndex + 1) % texts.length);\n    }, interval);\n\n    return () => window.clearInterval(timer);\n  }, [interval, texts.length]);\n\n  if (texts.length === 0) {\n    return null;\n  }\n\n  return (\n    <TextMorph\n      className={cn(\n        \"relative w-full min-w-0 inline-flex min-h-[1em] items-center justify-center xl:text-4xl md:text-3xl text-lg \",\n        className,\n      )}\n    >\n      {currentText}\n    </TextMorph>\n  );\n};\n","type":"registry:ui"}],"categories":["text","transition"],"type":"registry:ui"}