{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-picker",
  "title": "Model Picker",
  "description": "A searchable model selector grouped by provider, with capability icons, locked models and a detail pane showing context window, cost and capabilities.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "command",
    "popover"
  ],
  "files": [
    {
      "path": "registry/tealmac/model-picker.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Brain,\n  Check,\n  ChevronsUpDown,\n  Eye,\n  Lock,\n  Sparkles,\n  Wrench,\n  Zap,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\ntype ModelCapability = \"vision\" | \"reasoning\" | \"tools\" | \"fast\"\n\ntype ModelOption = {\n  /** Stable id returned by `onValueChange`. */\n  id: string\n  /** Name shown in the list and on the trigger. */\n  name: string\n  /** Provider or family. Options are grouped under it. */\n  provider?: string\n  /** One sentence on what the model is best at. */\n  description?: string\n  /** What the model can do. Shown as icons in the list and in words in the detail pane. */\n  capabilities?: ModelCapability[]\n  /** Context window in tokens, for example 200000. */\n  contextWindow?: number\n  /** Relative cost: 1 is cheapest, 3 is most expensive. */\n  cost?: 1 | 2 | 3\n  /** Marks a recent release. */\n  isNew?: boolean\n  /** The model cannot be picked, for example because it needs a higher plan. */\n  locked?: boolean\n  /** Text shown when locked, such as \"Pro\". */\n  lockedLabel?: string\n}\n\nconst CAPABILITY_META: Record<\n  ModelCapability,\n  {\n    label: string\n    help: string\n    icon: React.ComponentType<{ className?: string }>\n  }\n> = {\n  vision: { label: \"Vision\", help: \"Reads images and screenshots\", icon: Eye },\n  reasoning: {\n    label: \"Reasoning\",\n    help: \"Thinks before answering\",\n    icon: Brain,\n  },\n  tools: { label: \"Tools\", help: \"Calls functions and apps\", icon: Wrench },\n  fast: { label: \"Fast\", help: \"Low latency replies\", icon: Zap },\n}\n\nconst TONES = [\n  \"bg-chart-1\",\n  \"bg-chart-3\",\n  \"bg-chart-4\",\n  \"bg-chart-2\",\n  \"bg-chart-5\",\n]\n\nfunction hash(text: string) {\n  let h = 0\n  for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0\n  return h\n}\n\n/** 200000 → \"200k\", 1000000 → \"1M\". */\nfunction formatContext(tokens: number) {\n  if (tokens >= 1_000_000) return `${+(tokens / 1_000_000).toFixed(1)}M`\n  if (tokens >= 1000) return `${Math.round(tokens / 1000)}k`\n  return String(tokens)\n}\n\nfunction ProviderMark({\n  model,\n  className,\n}: {\n  model: ModelOption\n  className?: string\n}) {\n  const name = model.provider ?? model.name\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"text-background flex size-6 shrink-0 items-center justify-center rounded-md text-[11px] font-semibold uppercase\",\n        TONES[hash(name) % TONES.length],\n        className\n      )}\n    >\n      {name.charAt(0)}\n    </span>\n  )\n}\n\ntype ModelPickerProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n  /** The models to offer. Models that share a `provider` are grouped. */\n  models: ModelOption[]\n  /** Selected model id (controlled). */\n  value?: string\n  /** Initial model id when uncontrolled. */\n  defaultValue?: string\n  /** Called with the chosen id. */\n  onValueChange?: (id: string) => void\n  /** Shown on the trigger when nothing is selected. */\n  placeholder?: string\n  /** Placeholder of the search box. */\n  searchPlaceholder?: string\n  /** Show the search box. Defaults to on when there are more than six models. */\n  searchable?: boolean\n  /** Show the detail pane that follows the highlighted model on wider screens. */\n  showDetails?: boolean\n  /** Called when a locked model is chosen, for example to open an upgrade dialog. */\n  onLockedSelect?: (model: ModelOption) => void\n  /** Which side of the trigger the list aligns to. */\n  align?: \"start\" | \"center\" | \"end\"\n  /** Trigger style. \"compact\" hides the provider mark and is sized for a composer toolbar. */\n  variant?: \"default\" | \"compact\"\n}\n\nfunction ModelPicker({\n  models,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  placeholder,\n  searchPlaceholder,\n  searchable,\n  showDetails = true,\n  onLockedSelect,\n  align = \"start\",\n  variant = \"default\",\n  className,\n  disabled,\n  onKeyDown,\n  ...props\n}: ModelPickerProps) {\n  placeholder ??= \"Select a model\"\n  searchPlaceholder ??= \"Search models…\"\n  const [internal, setInternal] = React.useState(defaultValue)\n  const value = valueProp ?? internal\n  const [open, setOpen] = React.useState(false)\n  const selected = models.find((m) => m.id === value)\n  const [highlight, setHighlight] = React.useState<string>(\"\")\n  const hasSearch = searchable ?? models.length > 6\n  const listLabel = \"Models\"\n\n  const groups = React.useMemo(() => {\n    const map = new Map<string, ModelOption[]>()\n    for (const m of models) {\n      const key = m.provider ?? \"\"\n      map.set(key, [...(map.get(key) ?? []), m])\n    }\n    return [...map.entries()]\n  }, [models])\n\n  const detail = models.find((m) => m.id === highlight) ?? selected ?? models[0]\n\n  function pick(model: ModelOption) {\n    if (model.locked) {\n      onLockedSelect?.(model)\n      return\n    }\n    if (valueProp === undefined) setInternal(model.id)\n    onValueChange?.(model.id)\n    setOpen(false)\n  }\n\n  function handleOpenChange(next: boolean) {\n    setOpen(next)\n    if (next)\n      setHighlight(\n        selected && !selected.locked\n          ? selected.id\n          : (models.find((m) => !m.locked)?.id ?? \"\")\n      )\n  }\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      <PopoverTrigger\n        data-slot=\"model-picker\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        aria-label={selected ? `Model: ${selected.name}` : placeholder}\n        disabled={disabled}\n        onKeyDown={(e) => {\n          onKeyDown?.(e)\n          if (e.defaultPrevented) return\n          if (e.key === \"ArrowDown\" && !open) {\n            e.preventDefault()\n            handleOpenChange(true)\n          }\n        }}\n        className={cn(\n          \"bg-background text-foreground hover:bg-accent focus-visible:border-ring focus-visible:ring-ring/50 data-[state=open]:bg-accent inline-flex items-center gap-2 rounded-lg border text-sm font-medium shadow-xs transition-[color,background-color,border-color,box-shadow] duration-150 outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none\",\n          variant === \"compact\"\n            ? \"h-8 px-2.5 text-[13px]\"\n            : \"h-10 min-w-56 px-2.5\",\n          className\n        )}\n        {...props}\n      >\n        {variant === \"default\" && selected && <ProviderMark model={selected} />}\n        <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n          {selected?.name ?? placeholder}\n        </span>\n        <ChevronsUpDown\n          aria-hidden=\"true\"\n          className=\"text-muted-foreground ms-auto size-4 shrink-0\"\n        />\n      </PopoverTrigger>\n      <PopoverContent\n        aria-label=\"Choose a model\"\n        align={align}\n        sideOffset={6}\n        className={cn(\n          \"w-[min(23rem,calc(100vw-1.5rem))] gap-0 overflow-hidden p-0\",\n          showDetails && \"sm:w-[min(40rem,calc(100vw-1.5rem))]\"\n        )}\n      >\n        <div className=\"flex\">\n          <Command\n            label={listLabel}\n            value={highlight}\n            onValueChange={setHighlight}\n            className=\"min-w-0 flex-1 rounded-none bg-transparent\"\n          >\n            {hasSearch ? (\n              <CommandInput placeholder={searchPlaceholder} />\n            ) : (\n              // Keeps focus inside the list so arrow keys and type-ahead still work without a visible search box.\n              <div className=\"sr-only\">\n                <CommandInput placeholder={searchPlaceholder} />\n              </div>\n            )}\n            <CommandList className=\"max-h-80\">\n              <CommandEmpty>No model matches.</CommandEmpty>\n              {groups.map(([provider, items]) => (\n                <CommandGroup\n                  key={provider || \"all\"}\n                  heading={provider || undefined}\n                >\n                  {items.map((m) => (\n                    <CommandItem\n                      key={m.id}\n                      value={m.id}\n                      keywords={[\n                        m.name,\n                        m.provider ?? \"\",\n                        ...(m.capabilities ?? []),\n                      ]}\n                      data-checked={m.id === value ? \"true\" : undefined}\n                      onSelect={() => pick(m)}\n                      className=\"min-h-11 py-2\"\n                    >\n                      <ProviderMark model={m} />\n                      <span className=\"grid min-w-0 flex-1 gap-0.5\">\n                        <span className=\"flex items-center gap-1.5\">\n                          <span className=\"truncate\">{m.name}</span>\n                          {m.isNew && (\n                            <span className=\"bg-muted text-foreground inline-flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-px text-[10px] font-medium\">\n                              <Sparkles\n                                aria-hidden=\"true\"\n                                className=\"size-2.5\"\n                              />\n                              New\n                            </span>\n                          )}\n                          {m.locked && (\n                            <span className=\"text-foreground inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-px text-[10px] font-medium\">\n                              <Lock aria-hidden=\"true\" className=\"size-2.5\" />\n                              {m.lockedLabel ?? \"Locked\"}\n                            </span>\n                          )}\n                        </span>\n                        {m.description && (\n                          <span className=\"text-muted-foreground truncate text-xs\">\n                            {m.description}\n                          </span>\n                        )}\n                      </span>\n                      {m.id === value && (\n                        <Check\n                          aria-hidden=\"true\"\n                          className=\"text-foreground ms-auto size-4\"\n                        />\n                      )}\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              ))}\n            </CommandList>\n            <p\n              className=\"text-muted-foreground flex items-center gap-3 border-t px-3 py-2 text-xs\"\n              aria-hidden=\"true\"\n            >\n              <span>\n                <kbd className=\"font-mono\">↑↓</kbd> move\n              </span>\n              <span>\n                <kbd className=\"font-mono\">↵</kbd> select\n              </span>\n              <span>\n                <kbd className=\"font-mono\">esc</kbd> close\n              </span>\n            </p>\n          </Command>\n          {showDetails && detail && (\n            <aside\n              aria-live=\"polite\"\n              aria-label=\"Model details\"\n              className=\"bg-muted/30 hidden w-60 shrink-0 flex-col gap-4 border-s p-4 sm:flex\"\n            >\n              <div className=\"flex items-center gap-2.5\">\n                <ProviderMark model={detail} className=\"size-8 text-xs\" />\n                <div className=\"min-w-0\">\n                  <p className=\"text-foreground truncate text-sm font-medium\">\n                    {detail.name}\n                  </p>\n                  {detail.provider && (\n                    <p className=\"text-muted-foreground truncate text-xs\">\n                      {detail.provider}\n                    </p>\n                  )}\n                </div>\n              </div>\n              {detail.description && (\n                <p className=\"text-muted-foreground text-[13px] leading-5\">\n                  {detail.description}\n                </p>\n              )}\n              <dl className=\"grid gap-2 text-[13px]\">\n                {detail.contextWindow !== undefined && (\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <dt className=\"text-muted-foreground\">Context</dt>\n                    <dd className=\"text-foreground font-medium tabular-nums\">{`${formatContext(detail.contextWindow)} tokens`}</dd>\n                  </div>\n                )}\n                {detail.cost !== undefined && (\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <dt className=\"text-muted-foreground\">Cost</dt>\n                    <dd className=\"text-foreground font-medium\">\n                      <span aria-hidden=\"true\" className=\"tracking-widest\">\n                        {\"$\".repeat(detail.cost)}\n                        <span className=\"text-border\">\n                          {\"$\".repeat(3 - detail.cost)}\n                        </span>\n                      </span>\n                      <span className=\"sr-only\">{`${[\"Low\", \"Medium\", \"High\"][detail.cost - 1] ?? \"\"} cost`}</span>\n                    </dd>\n                  </div>\n                )}\n              </dl>\n              {detail.capabilities && detail.capabilities.length > 0 && (\n                <ul className=\"grid gap-2\">\n                  {detail.capabilities.map((c) => {\n                    const meta = CAPABILITY_META[c]\n                    const Icon = meta.icon\n                    return (\n                      <li\n                        key={c}\n                        className=\"flex items-start gap-2 text-[13px]\"\n                      >\n                        <Icon\n                          aria-hidden=\"true\"\n                          className=\"text-muted-foreground mt-0.5 size-3.5 shrink-0\"\n                        />\n                        <span>\n                          <span className=\"text-foreground font-medium\">\n                            {meta.label}\n                          </span>\n                          <span className=\"text-muted-foreground\">\n                            {\" \"}\n                            · {meta.help}\n                          </span>\n                        </span>\n                      </li>\n                    )\n                  })}\n                </ul>\n              )}\n              {detail.locked && (\n                <p className=\"bg-background text-foreground mt-auto flex items-center gap-1.5 rounded-lg border px-2.5 py-2 text-xs\">\n                  <Lock aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                  {`Available with ${detail.lockedLabel ?? \"a higher plan\"}`}\n                </p>\n              )}\n            </aside>\n          )}\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport {\n  ModelPicker,\n  formatContext,\n  type ModelPickerProps,\n  type ModelOption,\n  type ModelCapability,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}