{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-picker",
  "title": "Model Picker",
  "description": "A model chooser with room for what each one is good at.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/model-picker/model-picker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type Model = {\n  id: string\n  name: string\n  description?: string\n  /** Short capability tags, such as \"vision\" or \"fast\". */\n  badges?: readonly string[]\n  disabled?: boolean\n}\n\nexport type ModelPickerProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  models: readonly Model[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (id: string) => void\n  label?: string\n  placeholder?: string\n  disabled?: boolean\n}\n\nexport function ModelPicker({\n  models,\n  value,\n  defaultValue,\n  onValueChange,\n  label = \"Model\",\n  placeholder = \"Choose a model\",\n  disabled = false,\n  className,\n  ...rootProps\n}: ModelPickerProps) {\n  const id = React.useId()\n  const [open, setOpen] = React.useState(false)\n  const [ownValue, setOwnValue] = React.useState(defaultValue)\n  const [active, setActive] = React.useState(0)\n\n  const container = React.useRef<HTMLDivElement>(null)\n  const trigger = React.useRef<HTMLButtonElement>(null)\n  const list = React.useRef<HTMLUListElement>(null)\n\n  const controlled = value !== undefined\n  const selectedId = controlled ? value : ownValue\n  const selected = models.find((model) => model.id === selectedId)\n\n  const choosable = React.useMemo(\n    () => models.filter((model) => !model.disabled),\n    [models]\n  )\n\n  React.useEffect(() => {\n    if (!open) return\n\n    const onPointerDown = (event: PointerEvent) => {\n      if (!container.current?.contains(event.target as Node)) setOpen(false)\n    }\n\n    document.addEventListener(\"pointerdown\", onPointerDown)\n\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown)\n  }, [open])\n\n  React.useEffect(() => {\n    if (open) list.current?.focus()\n  }, [open])\n\n  function choose(model: Model) {\n    if (model.disabled) return\n\n    if (!controlled) setOwnValue(model.id)\n    onValueChange?.(model.id)\n    setOpen(false)\n    trigger.current?.focus()\n  }\n\n  function onKeyDown(event: React.KeyboardEvent) {\n    if (event.key === \"Escape\") {\n      event.preventDefault()\n      setOpen(false)\n      trigger.current?.focus()\n      return\n    }\n\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault()\n      const step = event.key === \"ArrowDown\" ? 1 : -1\n      setActive(\n        (current) =>\n          (current + step + choosable.length) % Math.max(1, choosable.length)\n      )\n      return\n    }\n\n    if (event.key === \"Home\" || event.key === \"End\") {\n      event.preventDefault()\n      setActive(event.key === \"Home\" ? 0 : choosable.length - 1)\n      return\n    }\n\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault()\n      const model = choosable[active]\n      if (model) choose(model)\n    }\n  }\n\n  const activeId = choosable[active]\n    ? `${id}-option-${choosable[active]!.id}`\n    : undefined\n\n  return (\n    <div\n      ref={container}\n      data-slot=\"model-picker\"\n      className={cn(\"relative min-w-0\", className)}\n      {...rootProps}\n    >\n      <button\n        ref={trigger}\n        type=\"button\"\n        id={`${id}-trigger`}\n        disabled={disabled}\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        aria-controls={open ? `${id}-list` : undefined}\n        aria-label={selected ? `${label}: ${selected.name}` : label}\n        className=\"border-border bg-card text-foreground hover:bg-muted/60 flex min-h-11 w-full items-center gap-2 rounded-[calc(var(--radius)+0.15rem)] border px-3 text-sm transition-colors duration-150 disabled:opacity-50 motion-reduce:transition-none\"\n        onClick={() => {\n          if (open) {\n            setOpen(false)\n            return\n          }\n\n          const index = choosable.findIndex((model) => model.id === selectedId)\n          setActive(index < 0 ? 0 : index)\n          setOpen(true)\n        }}\n      >\n        <span\n          className={cn(\n            \"min-w-0 flex-1 truncate text-left\",\n            !selected && \"text-muted-foreground\"\n          )}\n        >\n          {selected?.name ?? placeholder}\n        </span>\n        <ChevronsUpDown\n          aria-hidden=\"true\"\n          size={14}\n          className=\"text-muted-foreground shrink-0\"\n        />\n      </button>\n\n      {open ? (\n        <ul\n          ref={list}\n          id={`${id}-list`}\n          role=\"listbox\"\n          tabIndex={-1}\n          aria-label={label}\n          aria-activedescendant={activeId}\n          onKeyDown={onKeyDown}\n          className=\"border-border bg-card absolute z-50 mt-1 max-h-72 w-full overflow-auto rounded-[calc(var(--radius)+0.15rem)] border p-1 shadow-lg outline-none\"\n        >\n          {models.map((model) => {\n            const index = choosable.indexOf(model)\n            const isActive = index >= 0 && index === active\n            const isSelected = model.id === selectedId\n\n            return (\n              <li\n                key={model.id}\n                id={`${id}-option-${model.id}`}\n                role=\"option\"\n                aria-selected={isSelected}\n                aria-disabled={model.disabled || undefined}\n                data-active={isActive || undefined}\n                className={cn(\n                  \"flex min-h-11 cursor-pointer items-start gap-2 rounded-md px-2.5 py-2\",\n                  model.disabled && \"cursor-not-allowed opacity-50\",\n                  isActive && !model.disabled && \"bg-muted\"\n                )}\n                onPointerEnter={() => index >= 0 && setActive(index)}\n                onClick={() => choose(model)}\n              >\n                <Check\n                  aria-hidden=\"true\"\n                  size={14}\n                  className={cn(\n                    \"mt-1 shrink-0\",\n                    isSelected ? \"text-primary\" : \"invisible\"\n                  )}\n                />\n\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"flex flex-wrap items-center gap-1.5\">\n                    <span className=\"text-foreground text-sm\">\n                      {model.name}\n                    </span>\n                    {model.badges?.map((badge) => (\n                      <span\n                        key={badge}\n                        className=\"border-border text-muted-foreground rounded-full border px-1.5 py-px text-[0.625rem] leading-4\"\n                      >\n                        {badge}\n                      </span>\n                    ))}\n                  </span>\n                  {model.description ? (\n                    <span className=\"text-muted-foreground mt-0.5 block text-xs leading-snug\">\n                      {model.description}\n                    </span>\n                  ) : null}\n                </span>\n              </li>\n            )\n          })}\n        </ul>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "listbox",
    "select"
  ],
  "type": "registry:ui"
}