{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-device-select",
  "title": "Audio Device Select",
  "description": "A microphone, speaker or source picker with loading, permission, None and disconnected-device states, and a slot for a live preview.",
  "registryDependencies": [
    "select",
    "button"
  ],
  "files": [
    {
      "path": "components/ui/audio-device-select.tsx",
      "content": "\"use client\";\n\nimport { createContext, useContext, useMemo, useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\nconst NONE_VALUE = \"__none__\";\n\nexport type AudioDeviceStatus =\n  | \"available\"\n  | \"unavailable\"\n  | \"permission-required\";\n\nexport interface AudioDevice {\n  id: string;\n  label: string;\n  isDefault?: boolean;\n  status?: AudioDeviceStatus;\n  description?: string;\n}\n\ninterface DeviceItem {\n  value: string;\n  label: string;\n  device?: AudioDevice;\n  missing?: boolean;\n  none?: boolean;\n}\n\ninterface AudioDeviceSelectContextValue {\n  items: DeviceItem[];\n  loading: boolean;\n  permission: \"granted\" | \"prompt\" | \"denied\";\n  missing: boolean;\n  onRequestPermission?: () => void;\n}\n\nconst AudioDeviceSelectContext =\n  createContext<AudioDeviceSelectContextValue | null>(null);\n\nconst useAudioDeviceSelect = (part: string) => {\n  const context = useContext(AudioDeviceSelectContext);\n  if (!context) {\n    throw new Error(`${part} must be used inside AudioDeviceSelect.`);\n  }\n  return context;\n};\n\nexport const AudioDeviceSelectTrigger = ({\n  className,\n  ...props\n}: ComponentProps<typeof SelectTrigger>) => {\n  const { loading, missing, permission } = useAudioDeviceSelect(\n    \"AudioDeviceSelectTrigger\"\n  );\n  return (\n    <SelectTrigger\n      className={cn(\"w-full min-w-0\", className)}\n      data-loading={loading ? \"\" : undefined}\n      data-missing={missing ? \"\" : undefined}\n      data-permission={permission}\n      data-slot=\"audio-device-select-trigger\"\n      {...props}\n    />\n  );\n};\n\nexport interface AudioDeviceSelectValueProps extends Omit<\n  ComponentProps<typeof SelectValue>,\n  \"children\"\n> {\n  /** Shown with no selection. Default \"Select a device\". */\n  placeholder?: string;\n}\n\nexport const AudioDeviceSelectValue = ({\n  placeholder = \"Select a device\",\n  className,\n  ...props\n}: AudioDeviceSelectValueProps) => {\n  const { items, loading } = useAudioDeviceSelect(\"AudioDeviceSelectValue\");\n  return (\n    <SelectValue\n      className={cn(\"truncate\", className)}\n      data-slot=\"audio-device-select-value\"\n      {...props}\n    >\n      {(selected: string | null) => {\n        if (selected === null) {\n          return (\n            <span className=\"text-muted-foreground\">\n              {loading ? \"Finding devices…\" : placeholder}\n            </span>\n          );\n        }\n        return (\n          items.find((item) => item.value === selected)?.label ?? placeholder\n        );\n      }}\n    </SelectValue>\n  );\n};\n\nexport interface AudioDeviceSelectItemProps extends ComponentProps<\n  typeof SelectItem\n> {\n  device?: AudioDevice;\n}\n\nexport const AudioDeviceSelectItem = ({\n  device,\n  className,\n  children,\n  ...props\n}: AudioDeviceSelectItemProps) => (\n  <SelectItem\n    className={cn(\"items-start\", className)}\n    data-slot=\"audio-device-select-item\"\n    {...props}\n  >\n    {children ?? (\n      <span className=\"flex min-w-0 flex-col\">\n        <span className=\"flex items-center gap-2\">\n          <span className=\"truncate\">{device?.label}</span>\n          {device?.isDefault && !device.label.startsWith(\"Default\") ? (\n            <span className=\"text-muted-foreground text-xs\">Default</span>\n          ) : null}\n        </span>\n        {device?.description ? (\n          <span className=\"text-muted-foreground text-xs\">\n            {device.description}\n          </span>\n        ) : null}\n      </span>\n    )}\n  </SelectItem>\n);\n\nexport interface AudioDeviceSelectPermissionProps extends ComponentProps<\"div\"> {\n  /** Default: \"Allow microphone access to see your devices.\" */\n  message?: string;\n  /** Default: \"Allow access\". */\n  actionLabel?: string;\n}\n\nexport const AudioDeviceSelectPermission = ({\n  message,\n  actionLabel = \"Allow access\",\n  className,\n  ...props\n}: AudioDeviceSelectPermissionProps) => {\n  const { onRequestPermission, permission } = useAudioDeviceSelect(\n    \"AudioDeviceSelectPermission\"\n  );\n  const denied = permission === \"denied\";\n  const text =\n    message ??\n    (denied\n      ? \"Microphone access is blocked. Allow it in your browser's site settings.\"\n      : \"Allow microphone access to see your devices.\");\n\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground flex flex-col items-start gap-2 p-2 text-xs\",\n        className\n      )}\n      data-slot=\"audio-device-select-permission\"\n      {...props}\n    >\n      <p>{text}</p>\n      {!denied && onRequestPermission ? (\n        <Button onClick={onRequestPermission} size=\"xs\" variant=\"outline\">\n          {actionLabel}\n        </Button>\n      ) : null}\n    </div>\n  );\n};\n\nexport const AudioDeviceSelectContent = ({\n  children,\n  ...props\n}: ComponentProps<typeof SelectContent>) => {\n  const { items, loading, permission } = useAudioDeviceSelect(\n    \"AudioDeviceSelectContent\"\n  );\n  const needsPermission = permission !== \"granted\";\n\n  return (\n    <SelectContent\n      alignItemWithTrigger={false}\n      data-slot=\"audio-device-select-content\"\n      {...props}\n    >\n      {children ?? (\n        <SelectGroup>\n          {needsPermission ? <AudioDeviceSelectPermission /> : null}\n          {loading ? (\n            <SelectItem disabled value=\"__loading__\">\n              Finding devices…\n            </SelectItem>\n          ) : null}\n          {items.map((item) => {\n            if (item.none || item.missing) {\n              return (\n                <SelectItem\n                  disabled={item.missing}\n                  key={item.value}\n                  value={item.value}\n                >\n                  {item.label}\n                </SelectItem>\n              );\n            }\n            const status = item.device?.status ?? \"available\";\n            return (\n              <AudioDeviceSelectItem\n                device={item.device}\n                disabled={status !== \"available\"}\n                key={item.value}\n                value={item.value}\n              />\n            );\n          })}\n          {!loading && items.length === 0 && !needsPermission ? (\n            <p className=\"text-muted-foreground p-2 text-xs\">\n              No devices found.\n            </p>\n          ) : null}\n        </SelectGroup>\n      )}\n    </SelectContent>\n  );\n};\n\nexport const AudioDeviceSelectPreview = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\n      \"bg-muted/20 overflow-hidden rounded-lg border px-2\",\n      className\n    )}\n    data-slot=\"audio-device-select-preview\"\n    {...props}\n  />\n);\n\nexport interface AudioDeviceSelectProps {\n  devices: AudioDevice[];\n  /** The selected device id. `null` is no selection, or \"None\" with `allowNone`. */\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (id: string | null) => void;\n  /** Adds a \"None\" item. Default false. */\n  allowNone?: boolean;\n  /** Default \"None\". */\n  noneLabel?: string;\n  /** Shows \"Finding devices…\". Default false. */\n  loading?: boolean;\n  /** Microphone permission. Default `granted`. */\n  permission?: \"granted\" | \"prompt\" | \"denied\";\n  onRequestPermission?: () => void;\n  disabled?: boolean;\n  children?: ReactNode;\n}\n\nexport const AudioDeviceSelect = ({\n  devices,\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  allowNone = false,\n  noneLabel = \"None\",\n  loading = false,\n  permission = \"granted\",\n  onRequestPermission,\n  disabled,\n  children,\n}: AudioDeviceSelectProps) => {\n  const [uncontrolled, setUncontrolled] = useState<string | null>(defaultValue);\n  const value = valueProp === undefined ? uncontrolled : valueProp;\n  const [knownLabels, setKnownLabels] = useState<Record<string, string>>({});\n  if (devices.some((device) => knownLabels[device.id] !== device.label)) {\n    setKnownLabels((previous) => {\n      const next = { ...previous };\n      for (const device of devices) {\n        next[device.id] = device.label;\n      }\n      return next;\n    });\n  }\n\n  const missing =\n    value !== null &&\n    !loading &&\n    !devices.some((device) => device.id === value);\n\n  const items = useMemo<DeviceItem[]>(() => {\n    const list: DeviceItem[] = [];\n    if (allowNone) {\n      list.push({ label: noneLabel, none: true, value: NONE_VALUE });\n    }\n    for (const device of devices) {\n      list.push({ device, label: device.label, value: device.id });\n    }\n    if (missing && value !== null) {\n      const remembered = knownLabels[value] ?? \"Unknown device\";\n      list.push({\n        label: `${remembered} (disconnected)`,\n        missing: true,\n        value,\n      });\n    }\n    return list;\n  }, [allowNone, devices, knownLabels, missing, noneLabel, value]);\n\n  let selected: string | null = value;\n  if (value === null && allowNone) {\n    selected = NONE_VALUE;\n  }\n\n  const contextValue = useMemo<AudioDeviceSelectContextValue>(\n    () => ({ items, loading, missing, onRequestPermission, permission }),\n    [items, loading, missing, onRequestPermission, permission]\n  );\n\n  return (\n    <AudioDeviceSelectContext.Provider value={contextValue}>\n      <Select<string | null>\n        disabled={disabled}\n        onValueChange={(next) => {\n          const id = next === NONE_VALUE ? null : next;\n          if (valueProp === undefined) {\n            setUncontrolled(id);\n          }\n          onValueChange?.(id);\n        }}\n        value={selected}\n      >\n        {children ?? (\n          <>\n            <AudioDeviceSelectTrigger>\n              <AudioDeviceSelectValue />\n            </AudioDeviceSelectTrigger>\n            <AudioDeviceSelectContent />\n          </>\n        )}\n      </Select>\n    </AudioDeviceSelectContext.Provider>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}