{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "mic-selector", "registryDependencies": [ "button", "card", "dropdown-menu", "https://ui.elevenlabs.io/r/live-waveform.json" ], "files": [ { "path": "components/ui/mic-selector.tsx", "content": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\nimport { Check, ChevronsUpDown, Mic, MicOff } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { LiveWaveform } from \"@/components/ui/live-waveform\"\n\nexport interface AudioDevice {\n deviceId: string\n label: string\n groupId: string\n}\n\nexport interface MicSelectorProps {\n value?: string\n onValueChange?: (deviceId: string) => void\n muted?: boolean\n onMutedChange?: (muted: boolean) => void\n disabled?: boolean\n className?: string\n}\n\nexport function MicSelector({\n value,\n onValueChange,\n muted,\n onMutedChange,\n disabled,\n className,\n}: MicSelectorProps) {\n const { devices, loading, error, hasPermission, loadDevices } =\n useAudioDevices()\n const [selectedDevice, setSelectedDevice] = useState(value || \"\")\n const [internalMuted, setInternalMuted] = useState(false)\n const [isDropdownOpen, setIsDropdownOpen] = useState(false)\n\n // Use controlled muted if provided, otherwise use internal state\n const isMuted = muted !== undefined ? muted : internalMuted\n\n // Update internal state when controlled value changes\n useEffect(() => {\n if (value !== undefined) {\n setSelectedDevice(value)\n }\n }, [value])\n\n // Select first device by default\n const defaultDeviceId = devices[0]?.deviceId || \"\"\n useEffect(() => {\n if (!selectedDevice && defaultDeviceId) {\n const newDevice = defaultDeviceId\n setSelectedDevice(newDevice)\n onValueChange?.(newDevice)\n }\n }, [defaultDeviceId, selectedDevice, onValueChange])\n\n const currentDevice = devices.find((d) => d.deviceId === selectedDevice) ||\n devices[0] || {\n label: loading ? \"Loading...\" : \"No microphone\",\n deviceId: \"\",\n }\n\n const handleDeviceSelect = (deviceId: string, e?: React.MouseEvent) => {\n e?.preventDefault()\n setSelectedDevice(deviceId)\n onValueChange?.(deviceId)\n }\n\n const handleDropdownOpenChange = async (open: boolean) => {\n setIsDropdownOpen(open)\n if (open && !hasPermission && !loading) {\n await loadDevices()\n }\n }\n\n const toggleMute = () => {\n const newMuted = !isMuted\n if (muted === undefined) {\n setInternalMuted(newMuted)\n }\n onMutedChange?.(newMuted)\n }\n\n const isPreviewActive = isDropdownOpen && !isMuted\n\n return (\n \n \n \n {isMuted ? (\n \n ) : (\n \n )}\n \n {currentDevice.label}\n \n \n \n \n \n {loading ? (\n Loading devices...\n ) : error ? (\n Error: {error}\n ) : (\n devices.map((device) => (\n handleDeviceSelect(device.deviceId, e)}\n onSelect={(e) => e.preventDefault()}\n className=\"flex items-center justify-between\"\n >\n {device.label}\n {selectedDevice === device.deviceId && (\n \n )}\n \n ))\n )}\n {devices.length > 0 && (\n <>\n \n
\n {\n e.preventDefault()\n toggleMute()\n }}\n className=\"h-8 gap-2\"\n >\n {isMuted ? (\n \n ) : (\n \n )}\n {isMuted ? \"Unmute\" : \"Mute\"}\n \n
\n \n
\n
\n \n )}\n
\n
\n )\n}\n\nexport function useAudioDevices() {\n const [devices, setDevices] = useState([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState(null)\n const [hasPermission, setHasPermission] = useState(false)\n\n const loadDevicesWithoutPermission = useCallback(async () => {\n try {\n setLoading(true)\n setError(null)\n\n const deviceList = await navigator.mediaDevices.enumerateDevices()\n\n const audioInputs = deviceList\n .filter((device) => device.kind === \"audioinput\")\n .map((device) => {\n let cleanLabel =\n device.label || `Microphone ${device.deviceId.slice(0, 8)}`\n cleanLabel = cleanLabel.replace(/\\s*\\([^)]*\\)/g, \"\").trim()\n\n return {\n deviceId: device.deviceId,\n label: cleanLabel,\n groupId: device.groupId,\n }\n })\n\n setDevices(audioInputs)\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to get audio devices\"\n )\n console.error(\"Error getting audio devices:\", err)\n } finally {\n setLoading(false)\n }\n }, [])\n\n const loadDevicesWithPermission = useCallback(async () => {\n if (loading) return\n\n try {\n setLoading(true)\n setError(null)\n\n const tempStream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n })\n tempStream.getTracks().forEach((track) => track.stop())\n\n const deviceList = await navigator.mediaDevices.enumerateDevices()\n\n const audioInputs = deviceList\n .filter((device) => device.kind === \"audioinput\")\n .map((device) => {\n let cleanLabel =\n device.label || `Microphone ${device.deviceId.slice(0, 8)}`\n cleanLabel = cleanLabel.replace(/\\s*\\([^)]*\\)/g, \"\").trim()\n\n return {\n deviceId: device.deviceId,\n label: cleanLabel,\n groupId: device.groupId,\n }\n })\n\n setDevices(audioInputs)\n setHasPermission(true)\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to get audio devices\"\n )\n console.error(\"Error getting audio devices:\", err)\n } finally {\n setLoading(false)\n }\n }, [loading])\n\n useEffect(() => {\n loadDevicesWithoutPermission()\n }, [loadDevicesWithoutPermission])\n\n useEffect(() => {\n const handleDeviceChange = () => {\n if (hasPermission) {\n loadDevicesWithPermission()\n } else {\n loadDevicesWithoutPermission()\n }\n }\n\n navigator.mediaDevices.addEventListener(\"devicechange\", handleDeviceChange)\n\n return () => {\n navigator.mediaDevices.removeEventListener(\n \"devicechange\",\n handleDeviceChange\n )\n }\n }, [hasPermission, loadDevicesWithPermission, loadDevicesWithoutPermission])\n\n return {\n devices,\n loading,\n error,\n hasPermission,\n loadDevices: loadDevicesWithPermission,\n }\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }