{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "voice-chat-01",
	"title": "Voice Chat 01",
	"type": "registry:block",
	"description": "A full voice-and-text chat card: Orb header, scrollable conversation with copy actions, and a composer with send + call toggle.",
	"devDependencies": [
		"@lucide/svelte@^1.7.0"
	],
	"registryDependencies": [
		"button",
		"card",
		"https://sv11.ui.twango.dev/r/conversation.json",
		"input",
		"https://sv11.ui.twango.dev/r/message.json",
		"https://sv11.ui.twango.dev/r/orb.json",
		"https://sv11.ui.twango.dev/r/response.json",
		"https://sv11.ui.twango.dev/r/shimmering-text.json",
		"tooltip"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport AudioLinesIcon from \"@lucide/svelte/icons/audio-lines\";\n\timport CheckIcon from \"@lucide/svelte/icons/check\";\n\timport CopyIcon from \"@lucide/svelte/icons/copy\";\n\timport PhoneOffIcon from \"@lucide/svelte/icons/phone-off\";\n\timport SendIcon from \"@lucide/svelte/icons/send\";\n\timport { Button } from \"$UI$/button/index.js\";\n\timport { Card, CardContent, CardFooter, CardHeader } from \"$UI$/card/index.js\";\n\timport {\n\t\tConversation,\n\t\tConversationContent,\n\t\tConversationEmptyState,\n\t\tConversationScrollButton,\n\t} from \"$UI$/conversation/index.js\";\n\timport { Input } from \"$UI$/input/index.js\";\n\timport { Message, MessageContent } from \"$UI$/message/index.js\";\n\timport { Orb } from \"$UI$/orb/index.js\";\n\timport { Response } from \"$UI$/response/index.js\";\n\timport { ShimmeringText } from \"$UI$/shimmering-text/index.js\";\n\timport {\n\t\tTooltip,\n\t\tTooltipContent,\n\t\tTooltipProvider,\n\t\tTooltipTrigger,\n\t} from \"$UI$/tooltip/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\n\tlet { class: className }: { class?: string } = $props();\n\n\ttype ChatMessage = {\n\t\trole: \"user\" | \"assistant\";\n\t\tcontent: string;\n\t};\n\n\ttype AgentState = \"disconnected\" | \"connecting\" | \"connected\" | \"disconnecting\" | null;\n\n\tconst DEFAULT_AGENT = {\n\t\tname: \"Customer Support\",\n\t\tdescription: \"AI Voice Assistant\",\n\t};\n\n\tconst CANNED_REPLIES = [\n\t\t\"Thanks for reaching out — I can help with that. Can you share your order number?\",\n\t\t\"Got it. Let me pull that up for you.\",\n\t\t\"I just checked — your order is on track to ship within the next 1-2 business days.\",\n\t\t\"Anything else I can help with?\",\n\t];\n\n\tlet messages = $state<ChatMessage[]>([]);\n\tlet agentState = $state<AgentState>(\"disconnected\");\n\tlet textInput = $state(\"\");\n\tlet copiedIndex = $state<number | null>(null);\n\tlet errorMessage = $state<string | null>(null);\n\n\tlet timers: ReturnType<typeof setTimeout>[] = [];\n\tlet replyIndex = 0;\n\n\tfunction clearTimers() {\n\t\ttimers.forEach(clearTimeout);\n\t\ttimers = [];\n\t}\n\n\tfunction scheduleAssistantReply() {\n\t\tconst reply = CANNED_REPLIES[replyIndex % CANNED_REPLIES.length];\n\t\treplyIndex += 1;\n\t\ttimers.push(\n\t\t\tsetTimeout(() => {\n\t\t\t\tmessages = [...messages, { role: \"assistant\", content: reply }];\n\t\t\t}, 1200)\n\t\t);\n\t}\n\n\tfunction handleCall() {\n\t\tclearTimers();\n\t\tif (agentState === \"disconnected\" || agentState === null) {\n\t\t\terrorMessage = null;\n\t\t\tmessages = [];\n\t\t\tagentState = \"connecting\";\n\t\t\ttimers.push(\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tagentState = \"connected\";\n\t\t\t\t\tmessages = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\t\tcontent: \"Hi, you've reached customer support. How can I help you today?\",\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t}, 1500)\n\t\t\t);\n\t\t} else if (agentState === \"connected\") {\n\t\t\tagentState = \"disconnecting\";\n\t\t\ttimers.push(\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tagentState = \"disconnected\";\n\t\t\t\t\tmessages = [];\n\t\t\t\t}, 700)\n\t\t\t);\n\t\t}\n\t}\n\n\tfunction handleSendText() {\n\t\tconst messageToSend = textInput.trim();\n\t\tif (!messageToSend) return;\n\n\t\tif (agentState === \"disconnected\" || agentState === null) {\n\t\t\ttextInput = \"\";\n\t\t\tagentState = \"connecting\";\n\t\t\ttimers.push(\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tagentState = \"connected\";\n\t\t\t\t\tmessages = [{ role: \"user\", content: messageToSend }];\n\t\t\t\t\tscheduleAssistantReply();\n\t\t\t\t}, 800)\n\t\t\t);\n\t\t} else if (agentState === \"connected\") {\n\t\t\tmessages = [...messages, { role: \"user\", content: messageToSend }];\n\t\t\ttextInput = \"\";\n\t\t\tscheduleAssistantReply();\n\t\t}\n\t}\n\n\tfunction handleKeyDown(event: KeyboardEvent) {\n\t\tif (event.key === \"Enter\" && !event.shiftKey) {\n\t\t\tevent.preventDefault();\n\t\t\thandleSendText();\n\t\t}\n\t}\n\n\tasync function handleCopy(index: number, content: string) {\n\t\ttry {\n\t\t\tawait navigator.clipboard.writeText(content);\n\t\t} catch {\n\t\t\t// Insecure context or denied permission — don't flash the \"Copied!\"\n\t\t\t// state if the write actually failed.\n\t\t\treturn;\n\t\t}\n\t\tcopiedIndex = index;\n\t\ttimers.push(\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (copiedIndex === index) copiedIndex = null;\n\t\t\t}, 2000)\n\t\t);\n\t}\n\n\t$effect(() => {\n\t\treturn () => clearTimers();\n\t});\n\n\tconst isCallActive = $derived(agentState === \"connected\");\n\tconst isTransitioning = $derived(agentState === \"connecting\" || agentState === \"disconnecting\");\n\n\tfunction getInputVolume(): number {\n\t\tif (agentState !== \"connected\") return 0;\n\t\treturn 0.3 + 0.2 * Math.sin(performance.now() / 500);\n\t}\n\n\tfunction getOutputVolume(): number {\n\t\tif (agentState !== \"connected\") return 0;\n\t\treturn 0.3 + 0.2 * Math.sin(performance.now() / 700 + 1);\n\t}\n</script>\n\n<Card class={cn(\"mx-auto flex h-[380px] w-full flex-col gap-0 overflow-hidden\", className)}>\n\t<CardHeader class=\"flex shrink-0 flex-row items-center justify-between pb-4\">\n\t\t<div class=\"flex items-center gap-4\">\n\t\t\t<div class=\"ring-border relative size-10 overflow-hidden rounded-full ring-1\">\n\t\t\t\t<Orb class=\"h-full w-full\" volumeMode=\"manual\" {getInputVolume} {getOutputVolume} />\n\t\t\t</div>\n\t\t\t<div class=\"flex flex-col gap-0.5\">\n\t\t\t\t<p class=\"text-sm leading-none font-medium\">{DEFAULT_AGENT.name}</p>\n\t\t\t\t<div class=\"flex items-center gap-2\">\n\t\t\t\t\t{#if errorMessage}\n\t\t\t\t\t\t<p class=\"text-destructive text-xs\">{errorMessage}</p>\n\t\t\t\t\t{:else if agentState === \"disconnected\" || agentState === null}\n\t\t\t\t\t\t<p class=\"text-muted-foreground text-xs\">Tap to start voice chat</p>\n\t\t\t\t\t{:else if agentState === \"connected\"}\n\t\t\t\t\t\t<p class=\"text-xs text-green-600\">Connected</p>\n\t\t\t\t\t{:else if isTransitioning}\n\t\t\t\t\t\t<ShimmeringText text={agentState} class=\"text-xs capitalize\" />\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div\n\t\t\tclass={cn(\n\t\t\t\t\"flex h-2 w-2 rounded-full transition-all duration-300\",\n\t\t\t\tagentState === \"connected\" && \"bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]\",\n\t\t\t\tisTransitioning && \"animate-pulse bg-white/40\"\n\t\t\t)}\n\t\t></div>\n\t</CardHeader>\n\t<CardContent class=\"flex-1 overflow-hidden p-0\">\n\t\t<Conversation class=\"h-full\">\n\t\t\t<ConversationContent class=\"flex min-w-0 flex-col gap-2 p-6 pb-2\">\n\t\t\t\t{#if messages.length === 0}\n\t\t\t\t\t<ConversationEmptyState\n\t\t\t\t\t\ttitle={agentState === \"connecting\"\n\t\t\t\t\t\t\t? \"Starting conversation\"\n\t\t\t\t\t\t\t: agentState === \"connected\"\n\t\t\t\t\t\t\t\t? \"Start talking or type\"\n\t\t\t\t\t\t\t\t: \"Start a conversation\"}\n\t\t\t\t\t\tdescription={agentState === \"connecting\"\n\t\t\t\t\t\t\t? \"Connecting...\"\n\t\t\t\t\t\t\t: agentState === \"connected\"\n\t\t\t\t\t\t\t\t? \"Ready to chat\"\n\t\t\t\t\t\t\t\t: \"Type a message or tap the voice button\"}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#snippet icon()}\n\t\t\t\t\t\t\t<Orb class=\"size-12\" />\n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t</ConversationEmptyState>\n\t\t\t\t{:else}\n\t\t\t\t\t{#each messages as message, index (index)}\n\t\t\t\t\t\t<div class=\"flex w-full flex-col gap-1\">\n\t\t\t\t\t\t\t<Message from={message.role}>\n\t\t\t\t\t\t\t\t<MessageContent class=\"max-w-full min-w-0\">\n\t\t\t\t\t\t\t\t\t<Response\n\t\t\t\t\t\t\t\t\t\tcontent={message.content}\n\t\t\t\t\t\t\t\t\t\tclass=\"w-auto [overflow-wrap:anywhere] whitespace-pre-wrap\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</MessageContent>\n\t\t\t\t\t\t\t\t{#if message.role === \"assistant\"}\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclass=\"ring-border size-6 flex-shrink-0 self-end overflow-hidden rounded-full ring-1\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Orb\n\t\t\t\t\t\t\t\t\t\t\tclass=\"h-full w-full\"\n\t\t\t\t\t\t\t\t\t\t\tagentState={isCallActive && index === messages.length - 1 ? \"talking\" : null}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t\t{#if message.role === \"assistant\"}\n\t\t\t\t\t\t\t\t<div class=\"flex items-center gap-1\">\n\t\t\t\t\t\t\t\t\t<TooltipProvider>\n\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t\t{#snippet child({ props })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-muted-foreground hover:text-foreground relative size-9 p-1.5\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => handleCopy(index, message.content)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{#if copiedIndex === index}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<CheckIcon class=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<CopyIcon class=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"sr-only\">{copiedIndex === index ? \"Copied!\" : \"Copy\"}</span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<p>{copiedIndex === index ? \"Copied!\" : \"Copy\"}</p>\n\t\t\t\t\t\t\t\t\t\t\t</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t</TooltipProvider>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/each}\n\t\t\t\t{/if}\n\t\t\t</ConversationContent>\n\t\t\t<ConversationScrollButton />\n\t\t</Conversation>\n\t</CardContent>\n\t<CardFooter class=\"shrink-0 border-t\">\n\t\t<div class=\"flex w-full items-center gap-2\">\n\t\t\t<div class=\"flex flex-1 items-center gap-2\">\n\t\t\t\t<Input\n\t\t\t\t\tbind:value={textInput}\n\t\t\t\t\tonkeydown={handleKeyDown}\n\t\t\t\t\tplaceholder=\"Type a message...\"\n\t\t\t\t\tclass=\"h-9 focus-visible:ring-0 focus-visible:ring-offset-0\"\n\t\t\t\t\tdisabled={isTransitioning}\n\t\t\t\t/>\n\t\t\t\t<Button\n\t\t\t\t\tonclick={handleSendText}\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tclass=\"rounded-full\"\n\t\t\t\t\tdisabled={!textInput.trim() || isTransitioning}\n\t\t\t\t>\n\t\t\t\t\t<SendIcon class=\"size-4\" />\n\t\t\t\t\t<span class=\"sr-only\">Send message</span>\n\t\t\t\t</Button>\n\t\t\t\t{#if !isCallActive}\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonclick={handleCall}\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tclass={cn(\"relative shrink-0 rounded-full transition-all\")}\n\t\t\t\t\t\tdisabled={isTransitioning}\n\t\t\t\t\t>\n\t\t\t\t\t\t<AudioLinesIcon class=\"size-4\" />\n\t\t\t\t\t\t<span class=\"sr-only\">Start voice call</span>\n\t\t\t\t\t</Button>\n\t\t\t\t{:else}\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonclick={handleCall}\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\tclass={cn(\"relative shrink-0 rounded-full transition-all\")}\n\t\t\t\t\t\tdisabled={isTransitioning}\n\t\t\t\t\t>\n\t\t\t\t\t\t<PhoneOffIcon class=\"size-4\" />\n\t\t\t\t\t\t<span class=\"sr-only\">End call</span>\n\t\t\t\t\t</Button>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\t</CardFooter>\n</Card>\n",
			"type": "registry:block",
			"target": "components/blocks/voice-chat-01/voice-chat-01.svelte"
		},
		{
			"content": "import VoiceChat01 from \"./voice-chat-01.svelte\";\n\nexport { VoiceChat01, VoiceChat01 as default };\n",
			"type": "registry:block",
			"target": "components/blocks/voice-chat-01/index.ts"
		}
	]
}