{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-texts-morphing",
  "title": "Morphing Text",
  "description": "A text component that smoothly morphs characters to transition between strings.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@unlumen-ui/hooks-use-is-in-view"
  ],
  "files": [
    {
      "path": "registry/primitives/texts/morphing/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, type HTMLMotionProps } from \"motion/react\";\n\nimport {\n  useIsInView,\n  type UseIsInViewOptions,\n} from \"@/hooks/use-is-in-view\";\n\nfunction segmentGraphemes(text: string): string[] {\n  if (typeof Intl.Segmenter === \"function\") {\n    const seg = new Intl.Segmenter(undefined, {\n      granularity: \"grapheme\",\n    });\n    return Array.from(seg.segment(text), (s) => s.segment);\n  }\n  return Array.from(text);\n}\n\ntype MorphingTextProps = Omit<HTMLMotionProps<\"span\">, \"children\"> & {\n  delay?: number;\n  loop?: boolean;\n  holdDelay?: number;\n  text: string | string[];\n} & UseIsInViewOptions;\n\nfunction MorphingText({\n  ref,\n  text,\n  initial = { opacity: 0, scale: 0.8, filter: \"blur(10px)\" },\n  animate = { opacity: 1, scale: 1, filter: \"blur(0px)\" },\n  exit = { opacity: 0, scale: 0.8, filter: \"blur(10px)\" },\n  variants,\n  transition = { type: \"spring\", stiffness: 125, damping: 25, mass: 0.4 },\n  delay = 0,\n  inView = false,\n  inViewMargin = \"0px\",\n  inViewOnce = true,\n  loop = false,\n  holdDelay = 2500,\n  ...props\n}: MorphingTextProps) {\n  const { ref: localRef, isInView } = useIsInView(\n    ref as React.Ref<HTMLElement>,\n    {\n      inView,\n      inViewOnce,\n      inViewMargin,\n    },\n  );\n\n  const uniqueId = React.useId();\n\n  const [currentIndex, setCurrentIndex] = React.useState(0);\n  const [started, setStarted] = React.useState(false);\n\n  const currentText = React.useMemo(() => {\n    if (Array.isArray(text)) {\n      return text[currentIndex];\n    }\n    return text;\n  }, [text, currentIndex]);\n\n  const chars = React.useMemo(() => {\n    const graphemes = segmentGraphemes(currentText);\n    const counts = new Map<string, number>();\n    return graphemes.map((raw) => {\n      const key = raw.normalize(\"NFC\");\n      const n = (counts.get(key) ?? 0) + 1;\n      counts.set(key, n);\n      return {\n        layoutId: `${uniqueId}-${key}-${n}`,\n        label: key === \" \" ? \"\\u00A0\" : key,\n      };\n    });\n  }, [currentText, uniqueId]);\n\n  React.useEffect(() => {\n    if (isInView) {\n      const timeoutId = setTimeout(() => {\n        setStarted(true);\n      }, delay);\n      return () => clearTimeout(timeoutId);\n    }\n  }, [isInView, delay]);\n\n  React.useEffect(() => {\n    if (!started || !Array.isArray(text)) return;\n\n    let currentIndex = 0;\n\n    const interval = setInterval(() => {\n      currentIndex++;\n      if (currentIndex >= text.length) {\n        if (!loop) {\n          clearInterval(interval);\n          return;\n        } else {\n          currentIndex = 0;\n        }\n      }\n      setCurrentIndex(currentIndex);\n    }, holdDelay);\n\n    return () => clearInterval(interval);\n  }, [started, loop, text, holdDelay]);\n\n  return (\n    <motion.span ref={localRef} aria-label={currentText} {...props}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {chars.map((char) => (\n          <motion.span\n            key={char.layoutId}\n            layoutId={char.layoutId}\n            style={{ display: \"inline-block\" }}\n            aria-hidden=\"true\"\n            initial={initial}\n            animate={animate}\n            exit={exit}\n            variants={variants}\n            transition={transition}\n          >\n            {char.label}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n    </motion.span>\n  );\n}\n\nexport { MorphingText, type MorphingTextProps };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/primitives/texts/morphing.tsx"
    }
  ],
  "type": "registry:ui"
}