{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-texts-typing",
  "title": "Typing Text",
  "description": "A typing text animation.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@unlumen-ui/hooks-use-is-in-view",
    "@unlumen-ui/lib-get-strict-context"
  ],
  "files": [
    {
      "path": "registry/primitives/texts/typing/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, type HTMLMotionProps } from \"motion/react\";\n\nimport {\n  useIsInView,\n  type UseIsInViewOptions,\n} from \"@/hooks/use-is-in-view\";\nimport { getStrictContext } from \"@/lib/get-strict-context\";\n\ntype TypingTextContextType = {\n  isTyping: boolean;\n  setIsTyping: (isTyping: boolean) => void;\n};\n\nconst [TypingTextProvider, useTypingText] =\n  getStrictContext<TypingTextContextType>(\"TypingTextContext\");\n\ntype TypingTextProps = React.ComponentProps<\"span\"> & {\n  duration?: number;\n  delay?: number;\n  loop?: boolean;\n  holdDelay?: number;\n  text: string | string[];\n} & UseIsInViewOptions;\n\nfunction TypingText({\n  ref,\n  children,\n  duration = 100,\n  delay = 0,\n  inView = false,\n  inViewMargin = \"0px\",\n  inViewOnce = true,\n  loop = false,\n  holdDelay = 1000,\n  text,\n  ...props\n}: TypingTextProps) {\n  const { ref: localRef, isInView } = useIsInView(\n    ref as React.Ref<HTMLElement>,\n    {\n      inView,\n      inViewOnce,\n      inViewMargin,\n    },\n  );\n\n  const [isTyping, setIsTyping] = React.useState(false);\n  const [started, setStarted] = React.useState(false);\n  const [displayedText, setDisplayedText] = React.useState<string>(\"\");\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) return;\n\n    const timeoutIds: Array<ReturnType<typeof setTimeout>> = [];\n    const texts: string[] = typeof text === \"string\" ? [text] : text;\n\n    const typeText = (str: string, onComplete: () => void) => {\n      setIsTyping(true);\n      let currentIndex = 0;\n      const type = () => {\n        if (currentIndex <= str.length) {\n          setDisplayedText(str.substring(0, currentIndex));\n          currentIndex++;\n          const id = setTimeout(type, duration);\n          timeoutIds.push(id);\n        } else {\n          setIsTyping(false);\n          onComplete();\n        }\n      };\n      type();\n    };\n\n    const eraseText = (str: string, onComplete: () => void) => {\n      setIsTyping(true);\n      let currentIndex = str.length;\n      const erase = () => {\n        if (currentIndex >= 0) {\n          setDisplayedText(str.substring(0, currentIndex));\n          currentIndex--;\n          const id = setTimeout(erase, duration);\n          timeoutIds.push(id);\n        } else {\n          setIsTyping(false);\n          onComplete();\n        }\n      };\n      erase();\n    };\n\n    const animateTexts = (index: number) => {\n      typeText(texts[index] ?? \"\", () => {\n        const isLast = index === texts.length - 1;\n        if (isLast && !loop) {\n          return;\n        }\n        const id = setTimeout(() => {\n          eraseText(texts[index] ?? \"\", () => {\n            const nextIndex = isLast ? 0 : index + 1;\n            animateTexts(nextIndex);\n          });\n        }, holdDelay);\n        timeoutIds.push(id);\n      });\n    };\n\n    animateTexts(0);\n\n    return () => {\n      timeoutIds.forEach(clearTimeout);\n    };\n  }, [text, duration, started, loop, holdDelay]);\n\n  return (\n    <TypingTextProvider value={{ isTyping, setIsTyping }}>\n      <span ref={localRef} data-slot=\"typing-text\" {...props}>\n        <motion.span>{displayedText}</motion.span>\n        {children}\n      </span>\n    </TypingTextProvider>\n  );\n}\n\ntype TypingTextCursorProps = Omit<HTMLMotionProps<\"span\">, \"children\">;\n\nfunction TypingTextCursor({\n  style,\n  variants,\n  ...props\n}: TypingTextCursorProps) {\n  const { isTyping } = useTypingText();\n\n  return (\n    <motion.span\n      data-slot=\"typing-text-cursor\"\n      variants={{\n        blinking: {\n          opacity: [0, 0, 1, 1],\n          transition: {\n            duration: 1,\n            repeat: Infinity,\n            repeatDelay: 0,\n            ease: \"linear\",\n            times: [0, 0.5, 0.5, 1],\n          },\n        },\n        visible: {\n          opacity: 1,\n        },\n        ...variants,\n      }}\n      animate={isTyping ? \"visible\" : \"blinking\"}\n      style={{\n        display: \"inline-block\",\n        height: \"16px\",\n        transform: \"translateY(2px)\",\n        width: \"1px\",\n        backgroundColor: \"currentColor\",\n        ...style,\n      }}\n      {...props}\n    />\n  );\n}\n\nexport {\n  TypingText,\n  TypingTextCursor,\n  type TypingTextProps,\n  type TypingTextCursorProps,\n};\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/primitives/texts/typing.tsx"
    }
  ],
  "type": "registry:ui"
}