{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-device-select",
  "title": "Audio Device Select",
  "description": "Audio device picker with permission, loading and disconnected states.",
  "dependencies": [
    "solid-js@^1.9.15",
    "@kobalte/core@https://github.com/jhomra21/kobalte/releases/download/kobalte-solid1-audiocn-e0e3bf095f05c7e61230a251b79181c6d834d408/kobalte-core-0.13.14-audiocn.0.e0e3bf09.tgz"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/button",
    "@audiocn-solid/select"
  ],
  "files": [
    {
      "path": "components/ui/audio-device-select.tsx",
      "content": "import * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { SelectItemOptions } from \"@kobalte/core/select\";\nimport {\n  Show,\n  createContext,\n  createMemo,\n  createSignal,\n  useContext,\n} from \"solid-js\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectTrigger } from \"@/components/ui/select\";\nimport type {\n  SelectContentProps,\n  SelectTriggerProps,\n} from \"@/components/ui/select\";\nimport { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type {\n  DivDOMProps,\n  JSXElement,\n  SpanDOMProps,\n} from \"@/lib/solid/jsx-types\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { cn } from \"@/lib/utils\";\n\nconst NONE_VALUE = \"__none__\";\n\nconst EMPTY_COLLECTION_VALUE = \"__empty_collection__\";\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  emptyCollection?: boolean;\n}\n\ninterface DeviceContextValue {\n  readonly selected: DeviceItem | null;\n  readonly items: DeviceItem[];\n  readonly loading: boolean;\n  readonly permission: \"granted\" | \"prompt\" | \"denied\";\n  readonly missing: boolean;\n  readonly onRequestPermission?: () => void;\n}\n\nconst DeviceContext = createContext<DeviceContextValue>();\n\nconst useDeviceSelect = (part: string) => {\n  const context = useContext(DeviceContext);\n\n  if (!context)\n    throw new Error(`${part} must be used inside AudioDeviceSelect.`);\n\n  return context;\n};\n\nexport const AudioDeviceSelectTrigger = (props: SelectTriggerProps) => {\n  const settings = useDeviceSelect(\"AudioDeviceSelectTrigger\");\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <SelectTrigger\n      class={cn(\"w-full min-w-0\", props.class, props.className)}\n      data-loading={settings.loading ? \"\" : undefined}\n      data-missing={settings.missing ? \"\" : undefined}\n      data-placeholder={settings.selected ? undefined : \"\"}\n      data-permission={settings.permission}\n      data-slot=\"audio-device-select-trigger\"\n      {...rest}\n    />\n  );\n};\n\nexport interface AudioDeviceSelectValueProps extends Omit<\n  SpanDOMProps,\n  \"children\" | \"id\"\n> {\n  id?: string;\n  placeholder?: string;\n  className?: string;\n}\n\nexport const AudioDeviceSelectValue = (props: AudioDeviceSelectValueProps) => {\n  const settings = useDeviceSelect(\"AudioDeviceSelectValue\");\n  const select = SelectPrimitive.useSelectContext();\n  const id = () => props.id ?? select.generateId(\"value\");\n  createCompatEffect(id, select.registerValueId);\n  const rest = omitProps(props, [\"class\", \"className\", \"placeholder\", \"id\"]);\n\n  return (\n    <span\n      id={id()}\n      data-placeholder-shown={settings.selected ? undefined : \"\"}\n      class={cn(\"flex flex-1 truncate text-left\", props.class, props.className)}\n      data-slot=\"audio-device-select-value\"\n      {...rest}\n    >\n      {settings.selected?.label ??\n        (settings.loading\n          ? \"Finding devices…\"\n          : (props.placeholder ?? \"Select a device\"))}\n    </span>\n  );\n};\n\ntype NativeItemProps = Parameters<typeof SelectPrimitive.Item>[0];\n\nexport interface AudioDeviceSelectItemProps\n  extends NativeItemProps, SelectItemOptions {\n  device?: AudioDevice;\n  className?: string;\n}\n\nexport const AudioDeviceSelectItem = (props: AudioDeviceSelectItemProps) => {\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"device\",\n    \"children\",\n    \"item\",\n  ]);\n\n  return (\n    <SelectPrimitive.Item\n      item={props.item}\n      class={cn(\n        \"focus:bg-foreground/10 data-highlighted:bg-foreground/10 relative flex min-h-7 w-full cursor-default items-start gap-2 rounded-xl py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-device-select-item\"\n      {...rest}\n    >\n      <SelectPrimitive.ItemLabel class=\"flex flex-1 shrink-0 items-center gap-2 whitespace-nowrap\">\n        {props.children ?? (\n          <span class=\"flex min-w-0 flex-col\">\n            <span class=\"flex items-center gap-2\">\n              <span class=\"truncate\">{props.device?.label}</span>\n              <Show\n                when={\n                  props.device?.isDefault &&\n                  !props.device.label.startsWith(\"Default\")\n                }\n              >\n                <span class=\"text-muted-foreground text-xs\">Default</span>\n              </Show>\n            </span>\n            <Show when={props.device?.description}>\n              <span class=\"text-muted-foreground text-xs\">\n                {props.device?.description}\n              </span>\n            </Show>\n          </span>\n        )}\n      </SelectPrimitive.ItemLabel>\n      <SelectPrimitive.ItemIndicator class=\"pointer-events-none absolute right-2 flex size-4 items-center justify-center\">\n        <svg aria-hidden=\"true\" fill=\"currentColor\" viewBox=\"0 0 256 256\">\n          {/* Regular Phosphor Check, MIT © Phosphor Icons. */}\n          <path d=\"M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z\" />\n        </svg>\n      </SelectPrimitive.ItemIndicator>\n    </SelectPrimitive.Item>\n  );\n};\n\nexport interface AudioDeviceSelectPermissionProps extends DivDOMProps {\n  message?: string;\n  actionLabel?: string;\n  className?: string;\n}\n\nexport const AudioDeviceSelectPermission = (\n  props: AudioDeviceSelectPermissionProps\n) => {\n  const settings = useDeviceSelect(\"AudioDeviceSelectPermission\");\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"message\",\n    \"actionLabel\",\n  ]);\n\n  return (\n    <div\n      class={cn(\n        \"text-muted-foreground flex flex-col items-start gap-2 p-2 text-xs\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-device-select-permission\"\n      {...rest}\n    >\n      <p>\n        {props.message ??\n          (settings.permission === \"denied\"\n            ? \"Microphone access is blocked. Allow it in your browser's site settings.\"\n            : \"Allow microphone access to see your devices.\")}\n      </p>\n      <Show\n        when={settings.permission !== \"denied\" && settings.onRequestPermission}\n      >\n        <Button\n          onClick={() => settings.onRequestPermission?.()}\n          size=\"xs\"\n          variant=\"outline\"\n        >\n          {props.actionLabel ?? \"Allow access\"}\n        </Button>\n      </Show>\n    </div>\n  );\n};\n\nexport const AudioDeviceSelectContent = (props: SelectContentProps) => {\n  const settings = useDeviceSelect(\"AudioDeviceSelectContent\");\n  const rest = omitProps(props, [\"children\"]);\n\n  return (\n    <SelectContent data-slot=\"audio-device-select-content\" {...rest}>\n      {props.children ?? (\n        <>\n          <Show when={settings.permission !== \"granted\"}>\n            <AudioDeviceSelectPermission />\n          </Show>\n          <Show when={settings.loading}>\n            <p class=\"text-muted-foreground p-2 text-xs\">Finding devices…</p>\n          </Show>\n          <SelectPrimitive.Listbox />\n          <Show\n            when={\n              !settings.loading &&\n              !settings.items.length &&\n              settings.permission === \"granted\"\n            }\n          >\n            <p class=\"text-muted-foreground p-2 text-xs\">No devices found.</p>\n          </Show>\n        </>\n      )}\n    </SelectContent>\n  );\n};\n\nexport const AudioDeviceSelectPreview = (\n  props: DivDOMProps & { className?: string }\n) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <div\n      class={cn(\n        \"bg-muted/20 overflow-hidden rounded-lg border px-2\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-device-select-preview\"\n      {...rest}\n    />\n  );\n};\n\nexport interface AudioDeviceSelectProps {\n  devices: AudioDevice[];\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (id: string | null) => void;\n  allowNone?: boolean;\n  noneLabel?: string;\n  loading?: boolean;\n  permission?: \"granted\" | \"prompt\" | \"denied\";\n  onRequestPermission?: () => void;\n  disabled?: boolean;\n  children?: JSXElement;\n}\n\nexport const AudioDeviceSelect = (props: AudioDeviceSelectProps) => {\n  const [uncontrolled, setUncontrolled] = createSignal<string | null>(\n    props.defaultValue ?? null\n  );\n\n  const value = () =>\n    props.value === undefined ? uncontrolled() : props.value;\n\n  const knownLabels = new Map<string, string>();\n  createCompatEffect(\n    () => props.devices.map((device) => [device.id, device.label] as const),\n    (devices) => {\n      for (const [id, label] of devices) knownLabels.set(id, label);\n    }\n  );\n\n  const missing = () =>\n    value() !== null &&\n    !props.loading &&\n    !props.devices.some((device) => device.id === value());\n\n  const items = createMemo<DeviceItem[]>(() => {\n    const list: DeviceItem[] = [];\n\n    if (props.allowNone)\n      list.push({\n        value: NONE_VALUE,\n        label: props.noneLabel ?? \"None\",\n        none: true,\n      });\n\n    for (const device of props.devices)\n      list.push({ value: device.id, label: device.label, device });\n    const selected = value();\n\n    if (missing() && selected !== null)\n      list.push({\n        value: selected,\n        label: `${knownLabels.get(selected) ?? \"Unknown device\"} (disconnected)`,\n        missing: true,\n      });\n\n    return list;\n  });\n\n  const selectOptions = createMemo<DeviceItem[]>(() =>\n    items().length > 0\n      ? items()\n      : [\n          {\n            emptyCollection: true,\n            label: \"No selectable devices\",\n            value: EMPTY_COLLECTION_VALUE,\n          },\n        ]\n  );\n\n  const selected = () =>\n    items().find(\n      (item) =>\n        item.value ===\n        (value() === null && props.allowNone ? NONE_VALUE : value())\n    ) ?? null;\n\n  const settings: DeviceContextValue = {\n    get selected() {\n      return selected();\n    },\n    get items() {\n      return items();\n    },\n    get loading() {\n      return props.loading ?? false;\n    },\n    get permission() {\n      return props.permission ?? \"granted\";\n    },\n    get missing() {\n      return missing();\n    },\n    get onRequestPermission() {\n      return props.onRequestPermission;\n    },\n  };\n\n  return provideContext(DeviceContext, settings, () => (\n    <Select<DeviceItem>\n      options={selectOptions()}\n      optionValue=\"value\"\n      optionTextValue=\"label\"\n      optionDisabled={(item) =>\n        Boolean(item.emptyCollection) ||\n        Boolean(item.missing) ||\n        Boolean(item.device?.status && item.device.status !== \"available\")\n      }\n      value={selected()}\n      disabled={props.disabled}\n      placeholder={props.loading ? \"Finding devices…\" : \"Select a device\"}\n      onChange={(next) => {\n        if (next?.emptyCollection) return;\n\n        const id = !next || next.value === NONE_VALUE ? null : next.value;\n\n        if (props.value === undefined) setUncontrolled(id);\n        props.onValueChange?.(id);\n      }}\n      itemComponent={(itemProps) => (\n        <AudioDeviceSelectItem\n          aria-hidden={\n            itemProps.item.rawValue.emptyCollection ? \"true\" : undefined\n          }\n          class={itemProps.item.rawValue.emptyCollection ? \"hidden\" : undefined}\n          item={itemProps.item}\n          device={itemProps.item.rawValue.device}\n        >\n          {itemProps.item.rawValue.emptyCollection ||\n          itemProps.item.rawValue.none ||\n          itemProps.item.rawValue.missing\n            ? itemProps.item.rawValue.label\n            : undefined}\n        </AudioDeviceSelectItem>\n      )}\n    >\n      {props.children ?? (\n        <>\n          <AudioDeviceSelectTrigger>\n            <AudioDeviceSelectValue />\n          </AudioDeviceSelectTrigger>\n          <AudioDeviceSelectContent />\n        </>\n      )}\n    </Select>\n  ));\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
