{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal-text",
  "title": "Terminal Text",
  "author": "mrap10",
  "description": "Typewriter-style terminal text loop with blinking cursor and optional prefix.",
  "dependencies": ["motion"],
  "files": [
    {
      "path": "registry/default/text-effects/terminal-text.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TYPE_SPEED = 55;\nconst DELETE_SPEED = 28;\nconst HOLD_TIME = 850;\nconst SWITCH_DELAY = 120;\n\nexport const previewTexts = [\n  \"build things that matter\",\n  \"simplicity is the ultimate sophistication\",\n  \"ship it. then make it better.\",\n  \"who let the dogs out\",\n  \"woff woff woff woff\",\n];\n\ninterface TerminalTextProps {\n  texts?: string[];\n  prefix?: string;\n  className?: string;\n}\n\ntype TypeState = {\n  index: number;\n  visibleLength: number;\n  isDeleting: boolean;\n};\n\nfunction getNextState(state: TypeState, textLength: number, totalText: number) {\n  const { index, visibleLength, isDeleting } = state;\n\n  if (!isDeleting && visibleLength < textLength) {\n    return {\n      next: { ...state, visibleLength: visibleLength + 1 },\n      delay: TYPE_SPEED,\n    };\n  }\n  if (!isDeleting && visibleLength === textLength) {\n    return { next: { ...state, isDeleting: true }, delay: HOLD_TIME };\n  }\n  if (isDeleting && visibleLength > 0) {\n    return {\n      next: { ...state, visibleLength: visibleLength - 1 },\n      delay: DELETE_SPEED,\n    };\n  }\n  return {\n    next: {\n      index: (index + 1) % totalText,\n      visibleLength: 0,\n      isDeleting: false,\n    },\n    delay: SWITCH_DELAY,\n  };\n}\n\nexport function TerminalText({\n  texts = previewTexts,\n  prefix,\n  className = \"text-emerald-600 dark:text-emerald-400\",\n}: TerminalTextProps) {\n  const [state, setState] = useState<TypeState>({\n    index: 0,\n    visibleLength: 0,\n    isDeleting: false,\n  });\n\n  const resolvedTexts = texts.length > 0 ? texts : previewTexts;\n  const text = resolvedTexts[state.index % resolvedTexts.length];\n  const visibleText = text.slice(0, state.visibleLength);\n\n  useEffect(() => {\n    const { next, delay } = getNextState(\n      state,\n      text.length,\n      resolvedTexts.length\n    );\n    const id = setTimeout(() => setState(next), delay);\n    return () => clearTimeout(id);\n  }, [state, text, resolvedTexts.length]);\n\n  return (\n    <motion.div\n      aria-live=\"polite\"\n      className={cn(\"flex items-center font-mono tracking-[0.08em]\", className)}\n    >\n      <span className=\"whitespace-nowrap\">\n        {prefix && <span className=\"text-current/40\">{prefix}</span>}\n        {visibleText}\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ opacity: [1, 0, 1] }}\n          transition={{\n            duration: 0.9,\n            repeat: Infinity,\n            ease: \"linear\",\n          }}\n          className=\"inline-block h-[1em] w-[0.6ch] translate-y-[0.1em] border-r-8 border-current align-[-0.15em]\"\n        />\n      </span>\n    </motion.div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": ["text effects"],
  "type": "registry:component"
}
