{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "voice-picker",
	"title": "Voice Picker",
	"type": "registry:ui",
	"description": "A searchable voice selector with audio preview and orb visualization.",
	"dependencies": [
		"@lucide/svelte"
	],
	"devDependencies": [
		"@lucide/svelte@^1.7.0"
	],
	"registryDependencies": [
		"https://sv11.ui.twango.dev/r/audio-player.json",
		"button",
		"command",
		"https://sv11.ui.twango.dev/r/orb.json",
		"popover"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\timport type { Voice } from \"./types.js\";\n\n\texport type VoicePickerProps = {\n\t\t/** Voices to display in the picker. Any object conforming to `Voice` works. */\n\t\tvoices: Voice[];\n\t\t/** Selected voice ID for controlled usage. */\n\t\tvalue?: string;\n\t\t/** Called with the voice ID when the user picks a voice. */\n\t\tonValueChange?: (value: string) => void;\n\t\t/**\n\t\t * Placeholder shown on the trigger when no voice is selected.\n\t\t * @default \"Select a voice...\"\n\t\t */\n\t\tplaceholder?: string;\n\t\t/** Extra classes forwarded to the trigger button. */\n\t\tclass?: string;\n\t\t/** Controlled popover open state. Leave undefined for uncontrolled. */\n\t\topen?: boolean;\n\t\t/** Called when the popover open state changes. */\n\t\tonOpenChange?: (open: boolean) => void;\n\t};\n</script>\n\n<script lang=\"ts\">\n\timport ChevronsUpDownIcon from \"@lucide/svelte/icons/chevrons-up-down\";\n\timport { AudioPlayer } from \"../audio-player/index.js\";\n\timport { Button } from \"../button/index.js\";\n\timport * as Command from \"../command/index.js\";\n\timport { Orb } from \"../orb/index.js\";\n\timport * as Popover from \"../popover/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\timport VoicePickerItem from \"./voice-picker-item.svelte\";\n\n\tlet {\n\t\tvoices,\n\t\tvalue,\n\t\tonValueChange,\n\t\tplaceholder = \"Select a voice...\",\n\t\topen,\n\t\tonOpenChange,\n\t\tclass: className,\n\t}: VoicePickerProps = $props();\n\n\tlet internalOpen = $state(false);\n\tconst isOpenControlled = $derived(open !== undefined);\n\tconst isOpen = $derived(isOpenControlled ? open! : internalOpen);\n\n\tfunction setIsOpen(next: boolean) {\n\t\tif (isOpenControlled) onOpenChange?.(next);\n\t\telse internalOpen = next;\n\t}\n\n\tconst selectedVoice = $derived(voices.find((v) => v.id === value));\n</script>\n\n<AudioPlayer>\n\t<Popover.Root open={isOpen} onOpenChange={setIsOpen}>\n\t\t<Popover.Trigger>\n\t\t\t{#snippet child({ props })}\n\t\t\t\t<Button\n\t\t\t\t\t{...props}\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\taria-expanded={isOpen}\n\t\t\t\t\tclass={cn(\"w-full justify-between\", className)}\n\t\t\t\t>\n\t\t\t\t\t{#if selectedVoice}\n\t\t\t\t\t\t<div class=\"flex items-center gap-2 overflow-hidden\">\n\t\t\t\t\t\t\t<div class=\"relative size-6 shrink-0 overflow-visible\">\n\t\t\t\t\t\t\t\t<Orb agentState=\"thinking\" class=\"absolute inset-0\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<span class=\"truncate\">{selectedVoice.name}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t{placeholder}\n\t\t\t\t\t{/if}\n\t\t\t\t\t<ChevronsUpDownIcon class=\"ml-2 size-4 shrink-0 opacity-50\" />\n\t\t\t\t</Button>\n\t\t\t{/snippet}\n\t\t</Popover.Trigger>\n\t\t<Popover.Content class=\"w-[var(--bits-popover-anchor-width)] p-0\">\n\t\t\t<Command.Root>\n\t\t\t\t<Command.Input placeholder=\"Search voices...\" />\n\t\t\t\t<Command.List>\n\t\t\t\t\t<Command.Empty>No voice found.</Command.Empty>\n\t\t\t\t\t<Command.Group>\n\t\t\t\t\t\t{#each voices as voice (voice.id)}\n\t\t\t\t\t\t\t<VoicePickerItem\n\t\t\t\t\t\t\t\t{voice}\n\t\t\t\t\t\t\t\tisSelected={value === voice.id}\n\t\t\t\t\t\t\t\tonSelect={() => onValueChange?.(voice.id)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</Command.Group>\n\t\t\t\t</Command.List>\n\t\t\t</Command.Root>\n\t\t</Popover.Content>\n\t</Popover.Root>\n</AudioPlayer>\n",
			"type": "registry:ui",
			"target": "voice-picker/voice-picker.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport CheckIcon from \"@lucide/svelte/icons/check\";\n\timport PauseIcon from \"@lucide/svelte/icons/pause\";\n\timport PlayIcon from \"@lucide/svelte/icons/play\";\n\timport { useAudioPlayer } from \"../audio-player/context.svelte.js\";\n\timport type { AudioPlayerItem } from \"../audio-player/context.svelte.js\";\n\timport * as Command from \"../command/index.js\";\n\timport { Orb } from \"../orb/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\timport type { Voice } from \"./types.js\";\n\n\ttype Props = {\n\t\tvoice: Voice;\n\t\tisSelected: boolean;\n\t\tonSelect: () => void;\n\t};\n\n\tlet { voice, isSelected, onSelect }: Props = $props();\n\n\tconst player = useAudioPlayer<Voice>();\n\tlet isHovered = $state(false);\n\n\tconst preview = $derived(voice.previewUrl);\n\tconst audioItem = $derived<AudioPlayerItem<Voice> | null>(\n\t\tpreview ? { id: voice.id, src: preview, data: voice } : null\n\t);\n\tconst isPlaying = $derived(!!audioItem && player.isItemActive(audioItem.id) && player.isPlaying);\n\n\tconst keywords = $derived(\n\t\t[\n\t\t\tvoice.name,\n\t\t\tvoice.labels?.accent,\n\t\t\tvoice.labels?.gender,\n\t\t\tvoice.labels?.age,\n\t\t\tvoice.labels?.description,\n\t\t\tvoice.labels?.[\"use case\"],\n\t\t].filter((k): k is string => Boolean(k))\n\t);\n\n\tfunction handlePreview(e: MouseEvent) {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\tif (!audioItem) return;\n\t\tif (isPlaying) {\n\t\t\tplayer.pause();\n\t\t} else {\n\t\t\tplayer.play(audioItem);\n\t\t}\n\t}\n</script>\n\n<Command.Item\n\tvalue={voice.id}\n\t{keywords}\n\tonSelect={() => onSelect()}\n\tclass=\"flex items-center gap-3\"\n>\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"relative z-10 size-8 shrink-0 cursor-pointer overflow-visible border-0 bg-transparent p-0\"\n\t\tonmouseenter={() => (isHovered = true)}\n\t\tonmouseleave={() => (isHovered = false)}\n\t\tonclick={handlePreview}\n\t>\n\t\t<Orb agentState={isPlaying ? \"talking\" : null} class=\"pointer-events-none absolute inset-0\" />\n\t\t{#if preview && isHovered}\n\t\t\t<div\n\t\t\t\tclass=\"pointer-events-none absolute inset-0 flex size-8 shrink-0 items-center justify-center rounded-full bg-black/40 backdrop-blur-sm transition-opacity hover:bg-black/50\"\n\t\t\t>\n\t\t\t\t{#if isPlaying}\n\t\t\t\t\t<PauseIcon class=\"size-3 text-white\" />\n\t\t\t\t{:else}\n\t\t\t\t\t<PlayIcon class=\"size-3 text-white\" />\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t{/if}\n\t</button>\n\n\t<div class=\"flex flex-1 flex-col gap-0.5\">\n\t\t<span class=\"font-medium\">{voice.name}</span>\n\t\t{#if voice.labels}\n\t\t\t<div class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n\t\t\t\t{#if voice.labels.accent}<span>{voice.labels.accent}</span>{/if}\n\t\t\t\t{#if voice.labels.gender}<span>•</span><span class=\"capitalize\">{voice.labels.gender}</span\n\t\t\t\t\t>{/if}\n\t\t\t\t{#if voice.labels.age}<span>•</span><span class=\"capitalize\">{voice.labels.age}</span>{/if}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n\n\t<CheckIcon class={cn(\"ml-auto size-4 shrink-0\", isSelected ? \"opacity-100\" : \"opacity-0\")} />\n</Command.Item>\n",
			"type": "registry:ui",
			"target": "voice-picker/voice-picker-item.svelte"
		},
		{
			"content": "export type VoiceLabels = {\n\taccent?: string;\n\tgender?: string;\n\tage?: string;\n\tdescription?: string;\n\tdescriptive?: string;\n\t\"use case\"?: string;\n\tuse_case?: string;\n\t[key: string]: string | undefined;\n};\n\nexport type Voice = {\n\tid: string;\n\tname: string;\n\tpreviewUrl?: string;\n\tdescription?: string;\n\tlabels?: VoiceLabels;\n};\n",
			"type": "registry:ui",
			"target": "voice-picker/types.ts"
		},
		{
			"content": "import Root from \"./voice-picker.svelte\";\nimport Item from \"./voice-picker-item.svelte\";\n\nexport {\n\tRoot,\n\tItem,\n\t//\n\tRoot as VoicePicker,\n\tItem as VoicePickerItem,\n};\n\nexport type { VoicePickerProps } from \"./voice-picker.svelte\";\nexport type { Voice, VoiceLabels } from \"./types.js\";\n",
			"type": "registry:ui",
			"target": "voice-picker/index.ts"
		}
	]
}