{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "conversation-bar",
	"title": "Conversation Bar",
	"type": "registry:ui",
	"description": "A composable input bar for voice and text conversations with mic toggle, live waveform, and submit.",
	"dependencies": [
		"@lucide/svelte"
	],
	"devDependencies": [
		"@lucide/svelte@^1.7.0"
	],
	"registryDependencies": [
		"button",
		"card",
		"https://sv11.ui.twango.dev/r/live-waveform.json",
		"separator",
		"textarea"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { ConversationAdapter, ConversationMessage, AgentConnectionState } from \"./types.js\";\n\n\texport type ConversationBarProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & {\n\t\t/**\n\t\t * Backend bridge that owns the session lifecycle — connecting, streaming\n\t\t * messages, and muting. Conforms to\n\t\t * [`ConversationAdapter`](/docs/providers#conversationadapter).\n\t\t */\n\t\tadapter: ConversationAdapter;\n\t\t/**\n\t\t * Label shown in the waveform slot while the session is disconnected.\n\t\t * @default \"Customer Support\"\n\t\t */\n\t\tdisconnectedText?: string;\n\t\t/** Extra classes merged onto the inner waveform container. */\n\t\twaveformClassName?: string;\n\t\t/** Fired after the adapter reports a successful connection. */\n\t\tonConnect?: () => void;\n\t\t/** Fired after the adapter reports disconnection. */\n\t\tonDisconnect?: () => void;\n\t\t/** Fired when the adapter surfaces an error or `connect()` rejects. */\n\t\tonError?: (error: Error) => void;\n\t\t/** Fired for each message emitted by the adapter (both user and AI turns). */\n\t\tonMessage?: (message: ConversationMessage) => void;\n\t\t/** Fired after a text-input message has been handed off to the adapter. */\n\t\tonSendMessage?: (message: string) => void;\n\t\t/** Bindable ref to the root `<div>` element. */\n\t\tref?: HTMLDivElement | null;\n\t};\n</script>\n\n<script lang=\"ts\">\n\timport { onDestroy } from \"svelte\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { Button } from \"$UI$/button/index.js\";\n\timport { Card } from \"$UI$/card/index.js\";\n\timport { LiveWaveform } from \"$UI$/live-waveform/index.js\";\n\timport { Separator } from \"$UI$/separator/index.js\";\n\timport { Textarea } from \"$UI$/textarea/index.js\";\n\timport MicIcon from \"@lucide/svelte/icons/mic\";\n\timport MicOffIcon from \"@lucide/svelte/icons/mic-off\";\n\timport PhoneIcon from \"@lucide/svelte/icons/phone\";\n\timport XIcon from \"@lucide/svelte/icons/x\";\n\timport KeyboardIcon from \"@lucide/svelte/icons/keyboard\";\n\timport ChevronDownIcon from \"@lucide/svelte/icons/chevron-down\";\n\timport ArrowUpIcon from \"@lucide/svelte/icons/arrow-up\";\n\n\tlet {\n\t\tadapter,\n\t\tdisconnectedText = \"Customer Support\",\n\t\twaveformClassName,\n\t\tonConnect,\n\t\tonDisconnect,\n\t\tonError,\n\t\tonMessage,\n\t\tonSendMessage,\n\t\tclass: className,\n\t\tref = $bindable(null),\n\t\t...restProps\n\t}: ConversationBarProps = $props();\n\n\tlet isMuted = $state(false);\n\tlet agentState = $state<AgentConnectionState>(\"disconnected\");\n\tlet keyboardOpen = $state(false);\n\tlet textInput = $state(\"\");\n\tlet sessionId = 0;\n\n\tconst isConnected = $derived(agentState === \"connected\");\n\tconst isConnecting = $derived(agentState === \"connecting\");\n\n\tasync function startConversation() {\n\t\tif (agentState !== \"disconnected\") return;\n\t\tconst id = ++sessionId;\n\t\tagentState = \"connecting\";\n\n\t\ttry {\n\t\t\tawait adapter.connect({\n\t\t\t\tonConnect: () => {\n\t\t\t\t\tif (sessionId !== id) return;\n\t\t\t\t\tagentState = \"connected\";\n\t\t\t\t\tonConnect?.();\n\t\t\t\t},\n\t\t\t\tonDisconnect: () => {\n\t\t\t\t\tif (sessionId !== id) return;\n\t\t\t\t\tagentState = \"disconnected\";\n\t\t\t\t\tkeyboardOpen = false;\n\t\t\t\t\tonDisconnect?.();\n\t\t\t\t},\n\t\t\t\tonMessage: (message) => {\n\t\t\t\t\tif (sessionId !== id) return;\n\t\t\t\t\tonMessage?.(message);\n\t\t\t\t},\n\t\t\t\tonError: (error: Error) => {\n\t\t\t\t\tif (sessionId !== id) return;\n\t\t\t\t\tagentState = \"disconnected\";\n\t\t\t\t\tonError?.(error);\n\t\t\t\t},\n\t\t\t\tonStatusChange: (status) => {\n\t\t\t\t\tif (sessionId !== id) return;\n\t\t\t\t\tagentState = status;\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (sessionId !== id) return;\n\t\t\tagentState = \"disconnected\";\n\t\t\tconst error = err instanceof Error ? err : new Error(String(err));\n\t\t\tonError?.(error);\n\t\t}\n\t}\n\n\tfunction endConversation() {\n\t\t++sessionId;\n\t\tadapter.disconnect();\n\t\tagentState = \"disconnected\";\n\t\tkeyboardOpen = false;\n\t}\n\n\tfunction handleStartOrEnd() {\n\t\tif (isConnected || isConnecting) {\n\t\t\tendConversation();\n\t\t} else if (agentState === \"disconnected\") {\n\t\t\tstartConversation();\n\t\t}\n\t}\n\n\tfunction toggleMute() {\n\t\tif (!isConnected) return;\n\t\tisMuted = !isMuted;\n\t\tadapter.setMuted(isMuted);\n\t}\n\n\tfunction handleSendText() {\n\t\tif (!textInput.trim() || !isConnected) return;\n\t\tconst messageToSend = textInput;\n\t\tadapter.sendMessage(messageToSend);\n\t\ttextInput = \"\";\n\t\tonSendMessage?.(messageToSend);\n\t}\n\n\tfunction handleContextualUpdate() {\n\t\tif (textInput.trim() && isConnected) {\n\t\t\tadapter.sendContextualUpdate(textInput);\n\t\t}\n\t}\n\n\tfunction handleKeyDown(e: KeyboardEvent) {\n\t\tif (e.key === \"Enter\" && !e.shiftKey) {\n\t\t\te.preventDefault();\n\t\t\thandleSendText();\n\t\t}\n\t}\n\n\tonDestroy(() => {\n\t\tif (agentState !== \"disconnected\") {\n\t\t\tadapter.disconnect();\n\t\t}\n\t});\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"conversation-bar\"\n\tclass={cn(\"flex w-full items-end justify-center p-4\", className)}\n\t{...restProps}\n>\n\t<Card class=\"m-0 w-full gap-0 border p-0 shadow-lg\">\n\t\t<div class=\"flex flex-col-reverse\">\n\t\t\t<div>\n\t\t\t\t{#if keyboardOpen}\n\t\t\t\t\t<Separator />\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"flex items-center justify-between gap-2 p-2\">\n\t\t\t\t\t<div class=\"h-8 w-[120px] md:h-10\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\t\"flex h-full items-center gap-2 rounded-md py-1\",\n\t\t\t\t\t\t\t\t\"bg-foreground/5 text-foreground/70\"\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div class=\"h-full flex-1\">\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\t\t\t\"relative flex h-full w-full shrink-0 items-center justify-center overflow-hidden rounded-sm\",\n\t\t\t\t\t\t\t\t\t\twaveformClassName\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{#key agentState === \"disconnected\" ? \"idle\" : \"active\"}\n\t\t\t\t\t\t\t\t\t\t<LiveWaveform\n\t\t\t\t\t\t\t\t\t\t\tactive={isConnected && !isMuted}\n\t\t\t\t\t\t\t\t\t\t\tprocessing={isConnecting}\n\t\t\t\t\t\t\t\t\t\t\tbarWidth={3}\n\t\t\t\t\t\t\t\t\t\t\tbarGap={1}\n\t\t\t\t\t\t\t\t\t\t\tbarRadius={4}\n\t\t\t\t\t\t\t\t\t\t\tfadeEdges={true}\n\t\t\t\t\t\t\t\t\t\t\tfadeWidth={24}\n\t\t\t\t\t\t\t\t\t\t\tsensitivity={1.8}\n\t\t\t\t\t\t\t\t\t\t\tsmoothingTimeConstant={0.85}\n\t\t\t\t\t\t\t\t\t\t\theight={20}\n\t\t\t\t\t\t\t\t\t\t\tmode=\"static\"\n\t\t\t\t\t\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\"h-full w-full transition-opacity duration-300\",\n\t\t\t\t\t\t\t\t\t\t\t\tagentState === \"disconnected\" && \"opacity-0\"\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{/key}\n\t\t\t\t\t\t\t\t\t{#if agentState === \"disconnected\"}\n\t\t\t\t\t\t\t\t\t\t<div class=\"absolute inset-0 flex items-center justify-center\">\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"text-foreground/50 text-[10px] font-medium\">\n\t\t\t\t\t\t\t\t\t\t\t\t{disconnectedText}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"flex items-center\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonclick={toggleMute}\n\t\t\t\t\t\t\taria-pressed={isMuted}\n\t\t\t\t\t\t\taria-label={isMuted ? \"Unmute microphone\" : \"Mute microphone\"}\n\t\t\t\t\t\t\tclass={cn(isMuted ? \"bg-foreground/5\" : \"\")}\n\t\t\t\t\t\t\tdisabled={!isConnected}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{#if isMuted}\n\t\t\t\t\t\t\t\t<MicOffIcon />\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<MicIcon />\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonclick={() => (keyboardOpen = !keyboardOpen)}\n\t\t\t\t\t\t\taria-pressed={keyboardOpen}\n\t\t\t\t\t\t\taria-label={keyboardOpen ? \"Close text input\" : \"Open text input\"}\n\t\t\t\t\t\t\tclass=\"relative\"\n\t\t\t\t\t\t\tdisabled={!isConnected}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<KeyboardIcon\n\t\t\t\t\t\t\t\tclass={\"h-5 w-5 transform-gpu transition-all duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] \" +\n\t\t\t\t\t\t\t\t\t(keyboardOpen ? \"scale-75 opacity-0\" : \"scale-100 opacity-100\")}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<ChevronDownIcon\n\t\t\t\t\t\t\t\tclass={\"absolute inset-0 m-auto h-5 w-5 transform-gpu transition-all delay-50 duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] \" +\n\t\t\t\t\t\t\t\t\t(keyboardOpen ? \"scale-100 opacity-100\" : \"scale-75 opacity-0\")}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Separator orientation=\"vertical\" class=\"mx-1 -my-2.5\" />\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonclick={handleStartOrEnd}\n\t\t\t\t\t\t\taria-label={isConnected || isConnecting ? \"End conversation\" : \"Start conversation\"}\n\t\t\t\t\t\t\tdisabled={agentState === \"disconnecting\"}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{#if isConnected || isConnecting}\n\t\t\t\t\t\t\t\t<XIcon class=\"h-5 w-5\" />\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<PhoneIcon class=\"h-5 w-5\" />\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div\n\t\t\t\tclass={cn(\n\t\t\t\t\t\"overflow-hidden transition-all duration-300 ease-out\",\n\t\t\t\t\tkeyboardOpen ? \"max-h-[120px]\" : \"max-h-0\"\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<div class=\"relative px-2 pt-2 pb-2\">\n\t\t\t\t\t<Textarea\n\t\t\t\t\t\tbind:value={textInput}\n\t\t\t\t\t\toninput={handleContextualUpdate}\n\t\t\t\t\t\tonkeydown={handleKeyDown}\n\t\t\t\t\t\tplaceholder=\"Enter your message...\"\n\t\t\t\t\t\tclass=\"min-h-[100px] resize-none border-0 pr-12 shadow-none focus-visible:ring-0\"\n\t\t\t\t\t\tdisabled={!isConnected}\n\t\t\t\t\t/>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tonclick={handleSendText}\n\t\t\t\t\t\taria-label=\"Send message\"\n\t\t\t\t\t\tdisabled={!textInput.trim() || !isConnected}\n\t\t\t\t\t\tclass=\"absolute right-3 bottom-3 h-8 w-8\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<ArrowUpIcon class=\"h-4 w-4\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</Card>\n</div>\n",
			"type": "registry:ui",
			"target": "conversation-bar/conversation-bar.svelte"
		},
		{
			"content": "export type AgentConnectionState = \"disconnected\" | \"connecting\" | \"connected\" | \"disconnecting\";\n\nexport type ConversationMode = \"speaking\" | \"listening\";\n\nexport interface ConversationMessage {\n\tsource: \"user\" | \"ai\";\n\tmessage: string;\n}\n\nexport interface ConversationConfig {\n\tonConnect: () => void;\n\tonDisconnect: () => void;\n\tonMessage: (message: ConversationMessage) => void;\n\tonError: (error: Error) => void;\n\tonStatusChange: (status: AgentConnectionState) => void;\n\tonModeChange?: (mode: ConversationMode) => void;\n}\n\nexport interface ConversationAdapter {\n\tconnect(config: ConversationConfig): Promise<void>;\n\tdisconnect(): void;\n\tsendMessage(text: string): void;\n\tsendContextualUpdate(text: string): void;\n\tsetMuted(muted: boolean): void;\n}\n",
			"type": "registry:ui",
			"target": "conversation-bar/types.ts"
		},
		{
			"content": "import Root from \"./conversation-bar.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as ConversationBar,\n};\n\nexport type { ConversationBarProps } from \"./conversation-bar.svelte\";\n\nexport type {\n\tConversationAdapter,\n\tConversationConfig,\n\tConversationMessage,\n\tAgentConnectionState,\n\tConversationMode,\n} from \"./types.js\";\n",
			"type": "registry:ui",
			"target": "conversation-bar/index.ts"
		}
	]
}