{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-picker",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/model-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Combobox } from \"@base-ui/react/combobox\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ModelCapability = \"vision\" | \"reasoning\" | \"tools\" | \"fast\";\n\nexport type Model = {\n  /** Unique id sent to your backend, e.g. \"claude-sonnet-5-5\". */\n  value: string;\n  /** Display name. */\n  label: string;\n  provider: string;\n  description?: string;\n  /** Context window in tokens. */\n  contextWindow?: number;\n  capabilities?: ModelCapability[];\n};\n\ntype ModelGroup = { value: string; items: Model[] };\n\nconst capabilityLabel: Record<ModelCapability, string> = {\n  vision: \"Vision\",\n  reasoning: \"Reasoning\",\n  tools: \"Tools\",\n  fast: \"Fast\",\n};\n\nfunction formatContext(tokens: number) {\n  return tokens >= 1_000_000 ? `${+(tokens / 1_000_000).toFixed(1)}M` : `${Math.round(tokens / 1000)}K`;\n}\n\nfunction ProviderMark({ provider, className }: { provider: string; className?: string }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\"flex size-5 shrink-0 items-center justify-center rounded-sm bg-muted text-[10px] font-semibold uppercase text-muted-foreground\", className)}\n    >\n      {provider.charAt(0)}\n    </span>\n  );\n}\n\nfunction matches(model: Model, query: string) {\n  const q = query.trim().toLowerCase();\n  if (!q) return true;\n  return [model.label, model.provider, model.description ?? \"\", ...(model.capabilities ?? [])]\n    .join(\" \")\n    .toLowerCase()\n    .includes(q);\n}\n\nexport function ModelPicker({\n  models,\n  value,\n  defaultValue,\n  onValueChange,\n  placeholder = \"Select model\",\n  groupByProvider = true,\n  disabled,\n  className,\n}: {\n  models: Model[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  placeholder?: string;\n  /** Group the list under provider headings. */\n  groupByProvider?: boolean;\n  disabled?: boolean;\n  className?: string;\n}) {\n  const [inner, setInner] = React.useState(defaultValue ?? null);\n  const current = value !== undefined ? value : inner;\n  const selected = models.find((m) => m.value === current) ?? null;\n\n  const items = React.useMemo<Model[] | ModelGroup[]>(() => {\n    if (!groupByProvider) return models;\n    const map = new Map<string, Model[]>();\n    for (const m of models) map.set(m.provider, [...(map.get(m.provider) ?? []), m]);\n    return [...map].map(([provider, list]) => ({ value: provider, items: list }));\n  }, [models, groupByProvider]);\n\n  const renderItem = (model: Model) => (\n    <Combobox.Item\n      key={model.value}\n      value={model}\n      className=\"group/item flex cursor-default items-start gap-2.5 rounded-sm px-2 py-2 text-sm outline-none select-none data-[highlighted]:bg-muted data-[highlighted]:shadow-[inset_2px_0_0_currentColor]\"\n    >\n      <ProviderMark provider={model.provider} className=\"mt-0.5\" />\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"font-medium\">{model.label}</span>\n          {model.contextWindow && (\n            <span className=\"font-mono text-[11px] text-muted-foreground\">{formatContext(model.contextWindow)} ctx</span>\n          )}\n        </div>\n        {model.description && <p className=\"mt-0.5 text-xs leading-snug text-muted-foreground\">{model.description}</p>}\n        {!!model.capabilities?.length && (\n          <div className=\"mt-1.5 flex flex-wrap gap-1\">\n            {model.capabilities.map((c) => (\n              <span key={c} className=\"rounded-sm border px-1.5 py-px font-mono text-[10px] text-muted-foreground\">\n                {capabilityLabel[c]}\n              </span>\n            ))}\n          </div>\n        )}\n      </div>\n      <Combobox.ItemIndicator className=\"mt-0.5 text-foreground\">\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-3.5\"><path d=\"m3.5 8.5 3 3 6-6.5\" /></svg>\n      </Combobox.ItemIndicator>\n    </Combobox.Item>\n  );\n\n  return (\n    <Combobox.Root\n      items={items}\n      value={selected}\n      onValueChange={(m: Model | null) => {\n        if (!m) return;\n        setInner(m.value);\n        onValueChange?.(m.value);\n      }}\n      isItemEqualToValue={(a: Model, b: Model) => a.value === b.value}\n      filter={(item: Model, query: string) => matches(item, query)}\n      disabled={disabled}\n    >\n      <Combobox.Trigger\n        aria-label=\"Model\"\n        className={cn(\n          \"inline-flex h-8 items-center gap-2 rounded-sm px-2 text-sm text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 data-[popup-open]:bg-muted data-[popup-open]:text-foreground\",\n          className,\n        )}\n      >\n        {selected && <ProviderMark provider={selected.provider} className=\"size-4 text-[9px]\" />}\n        <span className=\"max-w-40 truncate\">{selected ? selected.label : placeholder}</span>\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-3.5\"><path d=\"m4.5 6.5 3.5 3.5 3.5-3.5\" /></svg>\n      </Combobox.Trigger>\n      <Combobox.Portal>\n        <Combobox.Positioner align=\"start\" sideOffset={6}>\n          <Combobox.Popup\n            aria-label=\"Select model\"\n            className=\"z-50 w-80 max-w-[var(--available-width)] origin-[var(--transform-origin)] overflow-hidden rounded border bg-background text-foreground shadow-lg transition-[scale,opacity] duration-150 motion-reduce:transition-none data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0\"\n          >\n            <div className=\"border-b p-2\">\n              <Combobox.Input\n                placeholder=\"Search models…\"\n                className=\"h-8 w-full rounded-sm bg-muted/60 px-2.5 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring\"\n              />\n            </div>\n            <Combobox.Empty>\n              <div className=\"px-3 py-6 text-center text-sm text-muted-foreground\">No models found.</div>\n            </Combobox.Empty>\n            <Combobox.List className=\"max-h-[min(22rem,var(--available-height))] overflow-y-auto overscroll-contain p-1 outline-none\">\n              {groupByProvider\n                ? (group: ModelGroup) => (\n                    <Combobox.Group key={group.value} items={group.items} className=\"block pb-1 last:pb-0\">\n                      <Combobox.GroupLabel className=\"px-2 py-1.5 text-xs font-medium text-muted-foreground select-none\">\n                        {group.value}\n                      </Combobox.GroupLabel>\n                      <Combobox.Collection>{renderItem}</Combobox.Collection>\n                    </Combobox.Group>\n                  )\n                : renderItem}\n            </Combobox.List>\n          </Combobox.Popup>\n        </Combobox.Positioner>\n      </Combobox.Portal>\n    </Combobox.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}