{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-3d-flip",
  "type": "registry:ui",
  "title": "Text 3D Flip",
  "description": "A text effect that flips each letter in 3D with a staggered animation on hover.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/magicui/text-3d-flip.tsx",
      "content": "\"use client\"\n\nimport React, {\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  type ElementType,\n} from \"react\"\nimport {\n  useAnimate,\n  type AnimationOptions,\n  type ValueAnimationTransition,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst HAS_SEGMENTER = typeof Intl !== \"undefined\" && \"Segmenter\" in Intl\n\nconst splitIntoCharacters = (text: string): string[] => {\n  if (HAS_SEGMENTER) {\n    const segmenter = new Intl.Segmenter(\"en\", { granularity: \"grapheme\" })\n    return Array.from(segmenter.segment(text), ({ segment }) => segment)\n  }\n  return Array.from(text)\n}\n\nconst extractTextFromChildren = (children: React.ReactNode): string => {\n  if (children == null) return \"\"\n  if (typeof children === \"string\") return children\n  if (typeof children === \"number\") return String(children)\n\n  if (Array.isArray(children)) {\n    return children.map(extractTextFromChildren).join(\"\")\n  }\n\n  if (React.isValidElement(children)) {\n    const props = children.props as Record<string, unknown>\n    const childText = props.children as React.ReactNode\n    if (childText != null) {\n      return extractTextFromChildren(childText)\n    }\n  }\n\n  return \"\"\n}\n\nconst ROTATION_MAP = {\n  top: \"rotateX(90deg)\",\n  right: \"rotateY(90deg)\",\n  bottom: \"rotateX(-90deg)\",\n  left: \"rotateY(-90deg)\",\n} as const\n\nconst DEFAULT_TRANSITION: ValueAnimationTransition = {\n  type: \"spring\",\n  damping: 30,\n  stiffness: 300,\n}\n\ninterface Text3DFlipProps {\n  children: React.ReactNode\n  as?: ElementType\n  className?: string\n  textClassName?: string\n  flipTextClassName?: string\n  staggerDuration?: number\n  staggerFrom?: \"first\" | \"last\" | \"center\" | number | \"random\"\n  transition?: ValueAnimationTransition | AnimationOptions\n  rotateDirection?: \"top\" | \"right\" | \"bottom\" | \"left\"\n}\n\nconst Text3DFlip = ({\n  children,\n  as: ElementTag = \"p\",\n  className,\n  textClassName,\n  flipTextClassName,\n  staggerDuration = 0.05,\n  staggerFrom = \"first\",\n  transition = DEFAULT_TRANSITION,\n  rotateDirection = \"right\",\n  ...props\n}: Text3DFlipProps) => {\n  const isAnimatingRef = useRef(false)\n  const isMountedRef = useRef(false)\n  const [scope, animate] = useAnimate()\n\n  const rotationTransform = ROTATION_MAP[rotateDirection]\n\n  useEffect(() => {\n    isMountedRef.current = true\n\n    return () => {\n      isMountedRef.current = false\n      isAnimatingRef.current = false\n    }\n  }, [])\n\n  const text = useMemo(() => {\n    try {\n      return extractTextFromChildren(children)\n    } catch {\n      return \"\"\n    }\n  }, [children])\n\n  const characters = useMemo(() => {\n    const words = text.split(\" \")\n    return words.map((word, i) => ({\n      characters: splitIntoCharacters(word),\n      needsSpace: i !== words.length - 1,\n    }))\n  }, [text])\n\n  const charOffsets = useMemo(() => {\n    const offsets = [0]\n    for (const word of characters) {\n      offsets.push(offsets.at(-1)! + word.characters.length)\n    }\n    return offsets\n  }, [characters])\n\n  const getStaggerDelay = useCallback(\n    (index: number, totalChars: number) => {\n      if (staggerFrom === \"first\") return index * staggerDuration\n      if (staggerFrom === \"last\")\n        return (totalChars - 1 - index) * staggerDuration\n      if (staggerFrom === \"center\") {\n        const center = Math.floor(totalChars / 2)\n        return Math.abs(center - index) * staggerDuration\n      }\n      if (staggerFrom === \"random\") {\n        const randomIndex = Math.floor(Math.random() * totalChars)\n        return Math.abs(randomIndex - index) * staggerDuration\n      }\n      return Math.abs(staggerFrom - index) * staggerDuration\n    },\n    [staggerFrom, staggerDuration]\n  )\n\n  const handleHoverStart = useCallback(async () => {\n    if (isAnimatingRef.current) return\n    isAnimatingRef.current = true\n\n    try {\n      const totalChars = characters.reduce(\n        (sum, word) => sum + word.characters.length,\n        0\n      )\n\n      const delays = Array.from({ length: totalChars }, (_, i) =>\n        getStaggerDelay(i, totalChars)\n      )\n\n      await animate(\n        \".text-3d-flip-char\",\n        { transform: rotationTransform },\n        {\n          ...transition,\n          delay: (i: number) => delays[i],\n        }\n      )\n\n      if (!isMountedRef.current) return\n\n      await animate(\n        \".text-3d-flip-char\",\n        { transform: \"rotateX(0deg) rotateY(0deg)\" },\n        { duration: 0 }\n      )\n    } finally {\n      if (isMountedRef.current) {\n        isAnimatingRef.current = false\n      }\n    }\n  }, [characters, transition, getStaggerDelay, rotationTransform, animate])\n\n  return (\n    <ElementTag\n      className={cn(\"relative flex flex-wrap\", className)}\n      onMouseEnter={handleHoverStart}\n      ref={scope}\n      {...props}\n    >\n      <span className=\"sr-only\">{text}</span>\n\n      {characters.map((wordObj, wordIndex) => (\n        <span key={wordIndex} className=\"inline-flex\">\n          {wordObj.characters.map((char, charIndex) => (\n            <CharBox\n              key={charOffsets[wordIndex] + charIndex}\n              char={char}\n              textClassName={textClassName}\n              flipTextClassName={flipTextClassName}\n              rotateDirection={rotateDirection}\n            />\n          ))}\n          {wordObj.needsSpace && <span className=\"whitespace-pre\"> </span>}\n        </span>\n      ))}\n    </ElementTag>\n  )\n}\n\ninterface CharBoxProps {\n  char: string\n  textClassName?: string\n  flipTextClassName?: string\n  rotateDirection: \"top\" | \"right\" | \"bottom\" | \"left\"\n}\n\nconst SECOND_FACE_TRANSFORMS = {\n  top: \"rotateX(-90deg) translateZ(0.5lh)\",\n  right:\n    \"rotateY(90deg) translateX(50%) rotateY(-90deg) translateX(-50%) rotateY(-90deg) translateX(50%)\",\n  bottom: \"rotateX(90deg) translateZ(0.5lh)\",\n  left: \"rotateY(90deg) translateX(50%) rotateY(-90deg) translateX(50%) rotateY(-90deg) translateX(50%)\",\n} as const\n\nconst FRONT_FACE_TRANSFORMS = {\n  top: \"translateZ(0.5lh)\",\n  bottom: \"translateZ(0.5lh)\",\n  left: \"rotateY(90deg) translateX(50%) rotateY(-90deg)\",\n  right: \"rotateY(-90deg) translateX(50%) rotateY(90deg)\",\n} as const\n\nconst CONTAINER_TRANSFORMS = {\n  top: \"translateZ(-0.5lh)\",\n  bottom: \"translateZ(-0.5lh)\",\n  left: \"rotateY(90deg) translateX(50%) rotateY(-90deg)\",\n  right: \"rotateY(90deg) translateX(50%) rotateY(-90deg)\",\n} as const\n\nconst CharBox = memo(\n  ({\n    char,\n    textClassName,\n    flipTextClassName,\n    rotateDirection,\n  }: CharBoxProps) => (\n    <span\n      className=\"text-3d-flip-char inline transform-3d\"\n      style={{ transform: CONTAINER_TRANSFORMS[rotateDirection] }}\n    >\n      <span\n        className={cn(\"relative h-[1lh] backface-hidden\", textClassName)}\n        style={{ transform: FRONT_FACE_TRANSFORMS[rotateDirection] }}\n      >\n        {char}\n      </span>\n      <span\n        className={cn(\n          \"absolute top-0 left-0 h-[1lh] backface-hidden\",\n          flipTextClassName\n        )}\n        style={{ transform: SECOND_FACE_TRANSFORMS[rotateDirection] }}\n      >\n        {char}\n      </span>\n    </span>\n  )\n)\n\nCharBox.displayName = \"CharBox\"\nText3DFlip.displayName = \"Text3DFlip\"\n\nexport default Text3DFlip\n",
      "type": "registry:ui"
    }
  ]
}