{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-mic-selector",
  "title": "AI: Mic Selector",
  "description": "A microphone device picker with explicit permission requests and unsupported/denied fallbacks.",
  "dependencies": [
    "@untitledui/icons@^0.0.23"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/command",
    "@nx-ui/popover",
    "@nx-ui/ai-use-controllable-state"
  ],
  "files": [
    {
      "path": "components/ai/mic-selector.tsx",
      "content": "// Ported from vercel/ai-elements@6a9d5b1822ffb10bba4bd97175f01edd7d8651cd.\n\"use client\";\n\nimport { useControllableState } from \"./use-controllable-state\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronSelectorVertical as ChevronsUpDownIcon } from \"@untitledui/icons\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nconst deviceIdRegex = /\\(([\\da-fA-F]{4}:[\\da-fA-F]{4})\\)$/;\n\ninterface MicSelectorContextType {\n  data: MediaDeviceInfo[];\n  error: string | null;\n  value: string | undefined;\n  onValueChange?: (value: string) => void;\n  open: boolean;\n  onOpenChange?: (open: boolean) => void;\n  width: number;\n  setWidth?: (width: number) => void;\n}\n\nconst MicSelectorContext = createContext<MicSelectorContextType>({\n  data: [],\n  error: null,\n  onOpenChange: undefined,\n  onValueChange: undefined,\n  open: false,\n  setWidth: undefined,\n  value: undefined,\n  width: 200,\n});\n\nexport const useAudioDevices = () => {\n  const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [hasPermission, setHasPermission] = useState(false);\n  const [permissionAttempted, setPermissionAttempted] = useState(false);\n\n  const loadDevicesWithoutPermission = useCallback(async () => {\n    if (!navigator.mediaDevices?.enumerateDevices) {\n      setError(\"Microphone selection is unavailable in this browser.\");\n      setLoading(false);\n      return;\n    }\n    try {\n      setLoading(true);\n      setError(null);\n\n      const deviceList = await navigator.mediaDevices.enumerateDevices();\n      const audioInputs = deviceList.filter(\n        (device) => device.kind === \"audioinput\"\n      );\n\n      setDevices(audioInputs);\n    } catch (caughtError) {\n      const message =\n        caughtError instanceof Error\n          ? caughtError.message\n          : \"Failed to get audio devices\";\n\n      setError(message);\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const loadDevicesWithPermission = useCallback(async () => {\n    if (loading) {\n      return;\n    }\n    setPermissionAttempted(true);\n    if (!navigator.mediaDevices?.getUserMedia) {\n      setError(\"Microphone access is unavailable in this browser.\");\n      return;\n    }\n\n    try {\n      setLoading(true);\n      setError(null);\n\n      const tempStream = await navigator.mediaDevices.getUserMedia({\n        audio: true,\n      });\n\n      for (const track of tempStream.getTracks()) {\n        track.stop();\n      }\n\n      const deviceList = await navigator.mediaDevices.enumerateDevices();\n      const audioInputs = deviceList.filter(\n        (device) => device.kind === \"audioinput\"\n      );\n\n      setDevices(audioInputs);\n      setHasPermission(true);\n    } catch (caughtError) {\n      const message =\n        caughtError instanceof Error\n          ? caughtError.message\n          : \"Failed to get audio devices\";\n\n      setError(message);\n    } finally {\n      setLoading(false);\n    }\n  }, [loading]);\n\n  useEffect(() => {\n    loadDevicesWithoutPermission();\n  }, [loadDevicesWithoutPermission]);\n\n  useEffect(() => {\n    if (!navigator.mediaDevices?.addEventListener) return;\n    const handleDeviceChange = () => {\n      // A device-change event must never trigger another permission prompt.\n      if (permissionAttempted && !hasPermission) return;\n      loadDevicesWithoutPermission();\n    };\n\n    navigator.mediaDevices.addEventListener(\"devicechange\", handleDeviceChange);\n\n    return () => {\n      navigator.mediaDevices.removeEventListener(\n        \"devicechange\",\n        handleDeviceChange\n      );\n    };\n  }, [hasPermission, loadDevicesWithoutPermission, permissionAttempted]);\n\n  return {\n    devices,\n    error,\n    hasPermission,\n    permissionAttempted,\n    loadDevices: loadDevicesWithPermission,\n    loading,\n  };\n};\n\nexport type MicSelectorProps = ComponentProps<typeof Popover> & {\n  defaultValue?: string;\n  value?: string | undefined;\n  onValueChange?: (value: string | undefined) => void;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nexport const MicSelector = ({\n  defaultValue,\n  value: controlledValue,\n  onValueChange: controlledOnValueChange,\n  defaultOpen = false,\n  open: controlledOpen,\n  onOpenChange: controlledOnOpenChange,\n  ...props\n}: MicSelectorProps) => {\n  const [value, onValueChange] = useControllableState<string | undefined>({\n    defaultProp: defaultValue,\n    onChange: controlledOnValueChange,\n    prop: controlledValue,\n  });\n  const [open, onOpenChange] = useControllableState({\n    defaultProp: defaultOpen,\n    onChange: controlledOnOpenChange,\n    prop: controlledOpen,\n  });\n  const [width, setWidth] = useState(200);\n  const { devices, error, loading, hasPermission, permissionAttempted, loadDevices } = useAudioDevices();\n\n  useEffect(() => {\n    if (open && !hasPermission && !permissionAttempted && !loading) {\n      loadDevices();\n    }\n  }, [open, hasPermission, permissionAttempted, loading, loadDevices]);\n\n  const contextValue = useMemo(\n    () => ({\n      data: devices,\n      error,\n      onOpenChange,\n      onValueChange,\n      open: open ?? false,\n      setWidth,\n      value,\n      width,\n    }),\n    [devices, error, onOpenChange, onValueChange, open, setWidth, value, width]\n  );\n\n  return (\n    <MicSelectorContext.Provider value={contextValue}>\n      <Popover {...props} onOpenChange={onOpenChange} open={open} />\n    </MicSelectorContext.Provider>\n  );\n};\n\nexport type MicSelectorTriggerProps = ComponentProps<typeof Button>;\n\nexport const MicSelectorTrigger = ({\n  children,\n  ...props\n}: MicSelectorTriggerProps) => {\n  const { setWidth } = useContext(MicSelectorContext);\n  const ref = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    // Create a ResizeObserver to detect width changes\n    const resizeObserver = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        const newWidth = (entry.target as HTMLElement).offsetWidth;\n        if (newWidth) {\n          setWidth?.(newWidth);\n        }\n      }\n    });\n\n    if (ref.current) {\n      resizeObserver.observe(ref.current);\n    }\n\n    // Clean up the observer when component unmounts\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [setWidth]);\n\n  return (\n    <PopoverTrigger render={<Button variant=\"outline\" {...props} ref={ref} />}>\n        {children}\n        <ChevronsUpDownIcon\n          className=\"shrink-0 text-muted-foreground\"\n          size={16}\n        />\n    </PopoverTrigger>\n  );\n};\n\nexport type MicSelectorContentProps = ComponentProps<typeof Command> & {\n  popoverOptions?: ComponentProps<typeof PopoverContent>;\n};\n\nexport const MicSelectorContent = ({\n  className,\n  popoverOptions,\n  ...props\n}: MicSelectorContentProps) => {\n  const { width, onValueChange, value, error } = useContext(MicSelectorContext);\n\n  return (\n    <PopoverContent\n      className={cn(\"p-0\", className)}\n      style={{ width }}\n      {...popoverOptions}\n    >\n      <Command onValueChange={onValueChange} value={value} {...props} />\n      {error && (\n        <p className=\"border-t px-3 py-2 text-sm text-destructive\" role=\"alert\">\n          {error}\n        </p>\n      )}\n    </PopoverContent>\n  );\n};\n\nexport type MicSelectorInputProps = ComponentProps<typeof CommandInput> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\n\nexport const MicSelectorInput = ({ ...props }: MicSelectorInputProps) => (\n  <CommandInput placeholder=\"Search microphones...\" {...props} />\n);\n\nexport type MicSelectorListProps = Omit<\n  ComponentProps<typeof CommandList>,\n  \"children\"\n> & {\n  children: (devices: MediaDeviceInfo[]) => ReactNode;\n};\n\nexport const MicSelectorList = ({\n  children,\n  ...props\n}: MicSelectorListProps) => {\n  const { data } = useContext(MicSelectorContext);\n\n  return <CommandList {...props}>{children(data)}</CommandList>;\n};\n\nexport type MicSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;\n\nexport const MicSelectorEmpty = ({\n  children,\n  ...props\n}: MicSelectorEmptyProps) => {\n  const { error } = useContext(MicSelectorContext);\n  if (error && children == null) return null;\n  return <CommandEmpty {...props}>{children ?? \"No microphone found.\"}</CommandEmpty>;\n};\n\nexport type MicSelectorItemProps = ComponentProps<typeof CommandItem>;\n\nexport const MicSelectorItem = (props: MicSelectorItemProps) => {\n  const { onValueChange, onOpenChange } = useContext(MicSelectorContext);\n\n  const handleSelect = useCallback(\n    (currentValue: string) => {\n      onValueChange?.(currentValue);\n      onOpenChange?.(false);\n    },\n    [onValueChange, onOpenChange]\n  );\n\n  return <CommandItem onSelect={handleSelect} {...props} />;\n};\n\nexport type MicSelectorLabelProps = ComponentProps<\"span\"> & {\n  device: MediaDeviceInfo;\n};\n\nexport const MicSelectorLabel = ({\n  device,\n  className,\n  ...props\n}: MicSelectorLabelProps) => {\n  const matches = device.label.match(deviceIdRegex);\n\n  if (!matches) {\n    return (\n      <span className={className} {...props}>\n        {device.label}\n      </span>\n    );\n  }\n\n  const [, deviceId] = matches;\n  const name = device.label.replace(deviceIdRegex, \"\");\n\n  return (\n    <span className={className} {...props}>\n      <span>{name}</span>\n      <span className=\"text-muted-foreground\"> ({deviceId})</span>\n    </span>\n  );\n};\n\nexport type MicSelectorValueProps = ComponentProps<\"span\">;\n\nexport const MicSelectorValue = ({\n  className,\n  ...props\n}: MicSelectorValueProps) => {\n  const { data, value } = useContext(MicSelectorContext);\n  const currentDevice = data.find((d) => d.deviceId === value);\n\n  if (!currentDevice) {\n    return (\n      <span className={cn(\"flex-1 text-left\", className)} {...props}>\n        Select microphone...\n      </span>\n    );\n  }\n\n  return (\n    <MicSelectorLabel\n      className={cn(\"flex-1 text-left\", className)}\n      device={currentDevice}\n      {...props}\n    />\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ai/mic-selector.tsx"
    }
  ],
  "type": "registry:ui"
}