{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "device-selector",
  "type": "registry:ui",
  "title": "Device Selector",
  "description": "Mic/camera/output picker on DropdownMenu, with an owned useMediaDevices hook.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/button.json",
    "https://seamui.dev/r/dropdown-menu.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/device-selector.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\ntype DeviceKind = \"audioinput\" | \"videoinput\" | \"audiooutput\"\ntype Device = { deviceId: string; label: string }\n\nconst KIND_LABEL: Record<DeviceKind, string> = {\n  audioinput: \"Microphone\",\n  videoinput: \"Camera\",\n  audiooutput: \"Speaker\",\n}\n\n/**\n * Owned device enumeration — lists the media devices of a kind and re-lists on\n * `devicechange`. Labels are empty until mic/camera permission is granted, so\n * unlabelled devices fall back to \"Microphone 2\"-style names. No dependency;\n * pass a `devices` prop to skip enumeration entirely.\n */\nfunction useMediaDevices(kind: DeviceKind): Device[] {\n  const [devices, setDevices] = React.useState<Device[]>([])\n\n  React.useEffect(() => {\n    const md =\n      typeof navigator !== \"undefined\" ? navigator.mediaDevices : undefined\n    if (!md?.enumerateDevices) return\n    let stopped = false\n    const load = async () => {\n      try {\n        const all = await md.enumerateDevices()\n        if (stopped) return\n        setDevices(\n          all\n            .filter((d) => d.kind === kind)\n            .map((d, i) => ({\n              deviceId: d.deviceId,\n              label: d.label || `${KIND_LABEL[kind]} ${i + 1}`,\n            }))\n        )\n      } catch {\n        // enumeration unavailable — leave the list empty\n      }\n    }\n    load()\n    md.addEventListener(\"devicechange\", load)\n    return () => {\n      stopped = true\n      md.removeEventListener(\"devicechange\", load)\n    }\n  }, [kind])\n\n  return devices\n}\n\ntype DeviceContextValue = {\n  kind: DeviceKind\n  devices: Device[]\n  value?: string\n  onValueChange: (v: string) => void\n}\nconst DeviceContext = React.createContext<DeviceContextValue | null>(null)\nfunction useDeviceContext() {\n  const ctx = React.useContext(DeviceContext)\n  if (!ctx)\n    throw new Error(\n      \"DeviceSelector parts must be used within <DeviceSelector>.\"\n    )\n  return ctx\n}\n\n// The device picker. Composes DropdownMenu — a radio group of devices with the\n// active one checked. Controlled via `value`/`onValueChange`, or self-lists via\n// the owned hook when no `devices` are passed.\nfunction DeviceSelector({\n  kind = \"audioinput\",\n  devices: devicesProp,\n  value,\n  defaultValue,\n  onValueChange,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenu> & {\n  kind?: DeviceKind\n  devices?: Device[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (deviceId: string) => void\n}) {\n  const enumerated = useMediaDevices(kind)\n  const devices = devicesProp ?? enumerated\n  const [internal, setInternal] = React.useState(defaultValue ?? \"\")\n  const current = value ?? internal\n  const set = (v: string) => {\n    if (value === undefined) setInternal(v)\n    onValueChange?.(v)\n  }\n\n  return (\n    <DeviceContext.Provider\n      value={{ kind, devices, value: current, onValueChange: set }}\n    >\n      <DropdownMenu {...props}>{children}</DropdownMenu>\n    </DeviceContext.Provider>\n  )\n}\n\n// A compact chevron key, sized to dock against a MediaToggle as a split control.\nfunction DeviceSelectorTrigger({\n  className,\n  \"aria-label\": ariaLabel,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuTrigger>) {\n  const { kind } = useDeviceContext()\n  return (\n    <DropdownMenuTrigger\n      data-slot=\"device-selector-trigger\"\n      aria-label={ariaLabel ?? `Select ${KIND_LABEL[kind].toLowerCase()}`}\n      render={\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className={cn(\"size-7 rounded-full\", className)}\n        />\n      }\n      {...props}\n    >\n      <ChevronDown className=\"size-4\" />\n    </DropdownMenuTrigger>\n  )\n}\n\nfunction DeviceSelectorContent({\n  className,\n  label,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuContent> & {\n  label?: React.ReactNode\n}) {\n  const { kind, devices, value, onValueChange } = useDeviceContext()\n  return (\n    <DropdownMenuContent\n      data-slot=\"device-selector-content\"\n      align=\"end\"\n      className={cn(\"min-w-56\", className)}\n      {...props}\n    >\n      <DropdownMenuLabel>{label ?? KIND_LABEL[kind]}</DropdownMenuLabel>\n      {devices.length === 0 ? (\n        <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n          No devices found\n        </div>\n      ) : (\n        <DropdownMenuRadioGroup value={value} onValueChange={onValueChange}>\n          {devices.map((d) => (\n            <DropdownMenuRadioItem key={d.deviceId} value={d.deviceId}>\n              {d.label}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n      )}\n    </DropdownMenuContent>\n  )\n}\n\nexport {\n  DeviceSelector,\n  DeviceSelectorTrigger,\n  DeviceSelectorContent,\n  useMediaDevices,\n}\n",
      "type": "registry:ui",
      "target": "components/ui/device-selector.tsx"
    }
  ]
}