{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-device-select",
  "type": "registry:component",
  "title": "AudioDeviceSelect",
  "description": "Composable audio input selector with permission, loading, default-device, and disconnected states.",
  "dependencies": [
    "lucide-react@^1.47.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/button.json",
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/select.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/audio-device-select-knobs.ts",
      "target": "@components/control-ui/knob-contracts/audio-device-select-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/audio-device-select.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const audioDeviceSelectKnobs = [\"--cui-audio-device-select-status-foreground\"] as const;\nexport type AudioDeviceSelectKnobStyle = Partial<Record<(typeof audioDeviceSelectKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/audio-device-select.tsx",
      "target": "@components/control-ui/audio-device-select.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { MicIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, ReactNode } from \"react\";\nimport { createContext, use, useState } from \"react\";\nimport type { AudioDeviceSelectKnobStyle } from \"@/components/control-ui/knob-contracts/audio-device-select-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport type { SelectProps, SelectTriggerProps } from \"@/components/control-ui/ui/select\";\nimport { Select, SelectContent, SelectItem, SelectTrigger } from \"@/components/control-ui/ui/select\";\n\nexport type AudioDevice = {\n  deviceId: string;\n  label: string;\n  isDefault?: boolean;\n  status?: \"available\" | \"unavailable\" | \"permission-required\";\n};\n\nexport type AudioDevicePermission = \"granted\" | \"prompt\" | \"denied\";\n\nexport type AudioDeviceSelectProps = Omit<SelectProps, \"items\" | \"children\"> & {\n  devices: readonly AudioDevice[];\n  loading?: boolean;\n  permission?: AudioDevicePermission;\n  onRequestPermission?: () => void | Promise<void>;\n  allowNone?: boolean;\n  noneLabel?: string;\n  label?: string;\n  error?: string | null;\n  className?: string;\n  children?: ReactNode;\n};\n\ntype AudioDeviceSelectContextValue = {\n  devices: readonly AudioDevice[];\n  selected: AudioDevice | null;\n  value: string | null;\n  missing: boolean;\n  loading: boolean;\n  permission: AudioDevicePermission;\n  disabled: boolean;\n  allowNone: boolean;\n  noneLabel: string;\n  label: string;\n  error: string | null;\n  onRequestPermission?: () => void | Promise<void>;\n};\n\nconst AudioDeviceSelectContext = createContext<AudioDeviceSelectContextValue | null>(null);\n\nfunction useAudioDeviceSelect() {\n  const context = use(AudioDeviceSelectContext);\n  if (!context) throw new Error(\"AudioDeviceSelect parts must be rendered inside AudioDeviceSelect.\");\n  return context;\n}\n\nfunction deviceLabel(device: AudioDevice, index = 0) {\n  return device.label.trim() || `Microphone ${index + 1}`;\n}\n\nexport function AudioDeviceSelect({\n  devices,\n  value,\n  defaultValue,\n  onValueChange,\n  disabled = false,\n  loading = false,\n  permission = \"granted\",\n  onRequestPermission,\n  allowNone = false,\n  noneLabel = \"No microphone\",\n  label = \"Microphone\",\n  error = null,\n  className,\n  children,\n  ...props\n}: AudioDeviceSelectProps) {\n  const [internalValue, setInternalValue] = useState<string | null>(() => defaultValue ?? null);\n  const current = value === undefined ? internalValue : value;\n  const connected = devices.find((device) => device.deviceId === current) ?? null;\n  const [remembered, setRemembered] = useState<AudioDevice | null>(connected);\n  if (connected && (remembered?.deviceId !== connected.deviceId || remembered?.label !== connected.label)) setRemembered(connected);\n  const missing = current !== null && !(allowNone && current === \"\") && !connected && !loading;\n  const selected = connected ?? (remembered?.deviceId === current ? remembered : null);\n\n  function handleValueChange(next: string) {\n    setInternalValue(next);\n    onValueChange?.(next);\n  }\n\n  return (\n    <AudioDeviceSelectContext.Provider\n      value={{\n        devices,\n        selected,\n        value: current,\n        missing,\n        loading,\n        permission,\n        disabled,\n        allowNone,\n        noneLabel,\n        label,\n        error,\n        onRequestPermission,\n      }}\n    >\n      <div\n        data-control-ui=\"audio-device-select\"\n        data-control-family=\"audio-device-select\"\n        data-slot=\"root\"\n        className={cn(\"grid min-w-0 gap-2\", className)}\n      >\n        <Select value={current} onValueChange={handleValueChange} disabled={disabled || loading} {...props}>\n          {children ?? (\n            <>\n              <AudioDeviceSelectTrigger />\n              <AudioDeviceSelectContent />\n              <AudioDeviceSelectStatus />\n              <AudioDeviceSelectPermission />\n            </>\n          )}\n        </Select>\n      </div>\n    </AudioDeviceSelectContext.Provider>\n  );\n}\n\nexport function AudioDeviceSelectTrigger({ children, className, ...props }: SelectTriggerProps) {\n  const context = useAudioDeviceSelect();\n  return (\n    <SelectTrigger\n      data-control-ui=\"audio-device-select\"\n      data-slot=\"trigger\"\n      data-loading={context.loading ? \"true\" : undefined}\n      data-permission={context.permission}\n      data-missing={context.missing ? \"true\" : undefined}\n      aria-label={context.label}\n      aria-busy={context.loading || undefined}\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <MicIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n          <AudioDeviceSelectValue />\n        </>\n      )}\n    </SelectTrigger>\n  );\n}\n\nexport function AudioDeviceSelectValue({ placeholder = \"Select a microphone\" }: { placeholder?: string }) {\n  const context = useAudioDeviceSelect();\n  let text = placeholder;\n  if (context.selected) text = deviceLabel(context.selected, context.devices.indexOf(context.selected));\n  if (context.allowNone && context.value === \"\") text = context.noneLabel;\n  if (context.missing) text = `${context.selected ? deviceLabel(context.selected) : \"Unknown device\"} (disconnected)`;\n  if (context.loading) text = \"Finding devices…\";\n  return (\n    <span data-control-ui=\"audio-device-select\" data-slot=\"value\" className=\"min-w-0 truncate\" title={text}>\n      {text}\n    </span>\n  );\n}\n\nexport function AudioDeviceSelectContent({ children, ...props }: ComponentProps<typeof SelectContent>) {\n  const context = useAudioDeviceSelect();\n  return (\n    <SelectContent data-control-ui=\"audio-device-select\" data-slot=\"content\" {...props}>\n      {children ?? (\n        <>\n          {context.allowNone ? <SelectItem value=\"\">{context.noneLabel}</SelectItem> : null}\n          {context.missing ? (\n            <SelectItem value={context.value ?? \"\"} disabled>\n              <AudioDeviceSelectValue />\n            </SelectItem>\n          ) : null}\n          {context.devices\n            .filter((device) => !context.allowNone || device.deviceId !== \"\")\n            .map((device, index) => (\n              <AudioDeviceSelectItem\n                key={device.deviceId}\n                value={device.deviceId}\n                label={deviceLabel(device, index)}\n                disabled={device.status === \"unavailable\" || device.status === \"permission-required\"}\n              >\n                <span className=\"min-w-0 flex-1 truncate\">{deviceLabel(device, index)}</span>\n                {device.isDefault || device.deviceId === \"default\" ? (\n                  <span className=\"ms-3 text-caption text-muted-foreground\">Default</span>\n                ) : null}\n              </AudioDeviceSelectItem>\n            ))}\n          {context.devices.length === 0 && !context.missing && !context.allowNone ? (\n            <SelectItem value=\"__audio-device-empty__\" disabled>\n              {context.permission === \"granted\" ? \"No microphones found\" : \"Allow microphone access to find devices\"}\n            </SelectItem>\n          ) : null}\n        </>\n      )}\n    </SelectContent>\n  );\n}\n\nexport function AudioDeviceSelectItem(props: ComponentProps<typeof SelectItem>) {\n  return <SelectItem data-control-ui=\"audio-device-select\" data-slot=\"item\" {...props} />;\n}\n\nexport function AudioDeviceSelectPreview({ className, ...props }: ComponentProps<\"div\">) {\n  return <div data-control-ui=\"audio-device-select\" data-slot=\"preview\" className={cn(\"min-w-0\", className)} {...props} />;\n}\n\nexport type AudioDeviceSelectStatusProps = Omit<ComponentProps<\"p\">, \"style\"> & {\n  style?: CSSProperties & AudioDeviceSelectKnobStyle;\n};\n\nexport function AudioDeviceSelectStatus({ className, ...props }: AudioDeviceSelectStatusProps) {\n  const context = useAudioDeviceSelect();\n  let message = context.error;\n  if (!message && context.permission === \"denied\") message = \"Microphone access is blocked. Allow it in your browser’s site settings.\";\n  if (!message && context.missing) message = \"This microphone is disconnected. Reconnect it or choose another device.\";\n  if (!message) return null;\n  return (\n    <p\n      data-control-ui=\"audio-device-select\"\n      data-control-family=\"audio-device-select\"\n      data-slot=\"status\"\n      role=\"status\"\n      className={cn(\"text-caption\", className)}\n      {...props}\n    >\n      {message}\n    </p>\n  );\n}\n\nexport function AudioDeviceSelectPermission({ children = \"Allow microphone access\", onClick, ...props }: ComponentProps<typeof Button>) {\n  const context = useAudioDeviceSelect();\n  if (context.permission !== \"prompt\" || !context.onRequestPermission) return null;\n  return (\n    <Button\n      data-control-ui=\"audio-device-select\"\n      data-slot=\"permission\"\n      variant=\"surface\"\n      size=\"sm\"\n      disabled={context.disabled || context.loading}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) void context.onRequestPermission?.();\n      }}\n      {...props}\n    >\n      {children}\n    </Button>\n  );\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/audio-device-select.css",
      "target": "@components/control-ui/styles/recipes/audio-device-select.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"audio-device-select\"][data-slot=\"root\"]) {\n    --cui-audio-device-select-status-foreground: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"audio-device-select\"][data-slot=\"status\"]) {\n    color: var(--cui-audio-device-select-status-foreground);\n  }\n}\n\n@property --cui-audio-device-select-status-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/audio-device-select.css\"": {}
  }
}
