{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "typing-animation",
	"title": "Typing Animation",
	"type": "registry:block",
	"description": "A typewriter effect component that types out text with customizable speed, cursor styles, and support for cycling through multiple words.",
	"dependencies": [
		"runed",
		"motion-sv"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { motion, type MotionProps, inView } from \"motion-sv\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { watch } from \"runed\";\n\n\tinterface TypingAnimationProps extends MotionProps {\n\t\tcontent?: string; // Single text string to type\n\t\twords?: string[]; // Array of words to cycle through\n\t\tclass?: string;\n\t\tduration?: number;\n\t\ttypeSpeed?: number;\n\t\tdeleteSpeed?: number;\n\t\tdelay?: number;\n\t\tpauseDelay?: number;\n\t\tloop?: boolean;\n\t\tstartOnView?: boolean;\n\t\tshowCursor?: boolean;\n\t\tblinkCursor?: boolean;\n\t\tcursorStyle?: \"line\" | \"block\" | \"underscore\";\n\t}\n\n\tlet {\n\t\tcontent,\n\t\twords,\n\t\tclass: className,\n\t\tduration = 100,\n\t\ttypeSpeed,\n\t\tdeleteSpeed,\n\t\tdelay = 0,\n\t\tpauseDelay = 1000,\n\t\tloop = false,\n\t\tstartOnView = true,\n\t\tshowCursor = true,\n\t\tblinkCursor = true,\n\t\tcursorStyle = \"line\",\n\t\t...props\n\t}: TypingAnimationProps = $props();\n\n\tlet displayedText = $state(\"\");\n\tlet currentWordIndex = $state(0);\n\tlet currentCharIndex = $state(0);\n\tlet phase = $state<\"typing\" | \"pause\" | \"deleting\">(\"typing\");\n\n\tlet elementRef: HTMLElement | null = $state(null);\n\n\tlet wordsToAnimate = $derived(words || (content ? [content] : []));\n\tlet hasMultipleWords = $derived(wordsToAnimate.length > 1);\n\tlet typingSpeed = $derived(typeSpeed || duration);\n\tlet deletingSpeed = $derived(deleteSpeed || typingSpeed / 2);\n\n\t// State: Track if element is in view\n\tlet isInView = $state(false);\n\n\t// Watch elementRef to setup inView detection\n\twatch(\n\t\t() => elementRef,\n\t\t() => {\n\t\t\tif (!startOnView) {\n\t\t\t\tisInView = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!elementRef) return;\n\n\t\t\t// Use motion-sv's inView for detection\n\t\t\tconst cleanup = inView(\n\t\t\t\telementRef,\n\t\t\t\t() => {\n\t\t\t\t\tisInView = true;\n\t\t\t\t},\n\t\t\t\t{ amount: 0.3 }\n\t\t\t);\n\n\t\t\treturn cleanup;\n\t\t}\n\t);\n\n\t// Derived: Should animation start\n\tlet shouldStart = $derived(startOnView ? isInView : true);\n\n\t// Animation loop using watch - only watches shouldStart\n\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n\tlet runAnimation = () => {\n\t\tif (!shouldStart || wordsToAnimate.length === 0) return;\n\n\t\tconst currentWord = wordsToAnimate[currentWordIndex] || \"\";\n\t\tconst graphemes = Array.from(currentWord);\n\n\t\t// Calculate delay based on current phase and initial delay\n\t\tconst timeoutDelay =\n\t\t\tdelay > 0 && displayedText === \"\"\n\t\t\t\t? delay\n\t\t\t\t: phase === \"typing\"\n\t\t\t\t\t? typingSpeed\n\t\t\t\t\t: phase === \"deleting\"\n\t\t\t\t\t\t? deletingSpeed\n\t\t\t\t\t\t: pauseDelay;\n\n\t\ttimeoutId = setTimeout(() => {\n\t\t\tswitch (phase) {\n\t\t\t\tcase \"typing\":\n\t\t\t\t\t// Type next character\n\t\t\t\t\tif (currentCharIndex < graphemes.length) {\n\t\t\t\t\t\tdisplayedText = graphemes.slice(0, currentCharIndex + 1).join(\"\");\n\t\t\t\t\t\tcurrentCharIndex = currentCharIndex + 1;\n\t\t\t\t\t\trunAnimation(); // Continue animation\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Finished typing current word\n\t\t\t\t\t\tif (hasMultipleWords || loop) {\n\t\t\t\t\t\t\tconst isLastWord = currentWordIndex === wordsToAnimate.length - 1;\n\t\t\t\t\t\t\tif (!isLastWord || loop) {\n\t\t\t\t\t\t\t\tphase = \"pause\";\n\t\t\t\t\t\t\t\trunAnimation(); // Continue animation\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"pause\":\n\t\t\t\t\t// Move to deleting phase after pause\n\t\t\t\t\tphase = \"deleting\";\n\t\t\t\t\trunAnimation(); // Continue animation\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"deleting\":\n\t\t\t\t\t// Delete previous character\n\t\t\t\t\tif (currentCharIndex > 0) {\n\t\t\t\t\t\tdisplayedText = graphemes.slice(0, currentCharIndex - 1).join(\"\");\n\t\t\t\t\t\tcurrentCharIndex = currentCharIndex - 1;\n\t\t\t\t\t\trunAnimation(); // Continue animation\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Finished deleting, move to next word\n\t\t\t\t\t\tconst nextIndex = (currentWordIndex + 1) % wordsToAnimate.length;\n\t\t\t\t\t\tcurrentWordIndex = nextIndex;\n\t\t\t\t\t\tphase = \"typing\";\n\t\t\t\t\t\trunAnimation(); // Continue animation\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}, timeoutDelay);\n\t};\n\n\t// Watch only shouldStart - animation state changes won't retrigger\n\twatch(\n\t\t() => shouldStart,\n\t\t() => {\n\t\t\t// Clear any existing timeout\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\n\t\t\t// Reset state\n\t\t\tdisplayedText = \"\";\n\t\t\tcurrentWordIndex = 0;\n\t\t\tcurrentCharIndex = 0;\n\t\t\tphase = \"typing\";\n\n\t\t\t// Start animation if shouldStart is true\n\t\t\tif (shouldStart && wordsToAnimate.length > 0) {\n\t\t\t\trunAnimation();\n\t\t\t}\n\t\t}\n\t);\n\n\t// Derived: Current word as array of graphemes\n\tlet currentWordGraphemes = $derived(Array.from(wordsToAnimate[currentWordIndex] || \"\"));\n\n\t// Derived: Animation is complete (not looping and finished last word)\n\tlet isComplete = $derived(\n\t\t!loop &&\n\t\t\tcurrentWordIndex === wordsToAnimate.length - 1 &&\n\t\t\tcurrentCharIndex >= currentWordGraphemes.length &&\n\t\t\tphase !== \"deleting\"\n\t);\n\n\t// Derived: Should show cursor\n\tlet shouldShowCursor = $derived(\n\t\tshowCursor &&\n\t\t\t!isComplete &&\n\t\t\t(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)\n\t);\n\n\t// Helper: Get cursor character based on style\n\tfunction getCursorChar() {\n\t\tswitch (cursorStyle) {\n\t\t\tcase \"block\":\n\t\t\t\treturn \"▌\";\n\t\t\tcase \"underscore\":\n\t\t\t\treturn \"_\";\n\t\t\tcase \"line\":\n\t\t\tdefault:\n\t\t\t\treturn \"|\";\n\t\t}\n\t}\n</script>\n\n<!-- Render motion component as span -->\n<motion.span\n\tbind:ref={elementRef}\n\tclass={cn(\"leading-20 tracking-[-0.02em]\", className)}\n\t{...props}\n>\n\t{displayedText}\n\t{#if shouldShowCursor}\n\t\t<span class={cn(\"inline-block\", blinkCursor && \"animate-blink-cursor\")}>\n\t\t\t{getCursorChar()}\n\t\t</span>\n\t{/if}\n</motion.span>\n",
			"type": "registry:component",
			"target": "magic/typing-animation/typing-animation.svelte"
		},
		{
			"content": "import TypingAnimation from \"./typing-animation.svelte\";\nexport { TypingAnimation };\n",
			"type": "registry:file",
			"target": "magic/typing-animation/index.ts"
		}
	]
}