{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-selector",
  "title": "Model Selector",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "Searchable model picker for AI chat: a dialog opened from any trigger that hosts a cmdk command palette with a whole-term search input, grouped items, separators, keyboard-shortcut hints, an announced empty state, and decorative provider logos from models.dev or a custom src (ModelSelectorLogo, ModelSelectorLogoGroup, ModelSelectorName). Use to switch the model behind a conversation or prompt input.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "command",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/ai/model-selector.tsx",
      "content": "// Derived from Vercel AI Elements model-selector.tsx (Apache-2.0, Copyright 2023 Vercel, Inc.).\n// Modified for uifiles: ported from Radix UI to Base UI; dependencies point at @uifiles.\n\"use client\"\n\nimport { cn } from \"cn\"\nimport type { ComponentProps, ReactNode } from \"react\"\nimport { createContext, useContext, useState } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport {\n  Command,\n  CommandDialog,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n  CommandShortcut,\n} from \"@/components/ui/command\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\"\n\nexport type ModelSelectorProps = ComponentProps<typeof Dialog>\n\nexport const ModelSelector = (props: ModelSelectorProps) => (\n  <Dialog {...props} />\n)\n\nexport type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>\n\n// Base UI: compose a custom trigger with `render={<Button />}` instead of `asChild`.\nexport const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (\n  <DialogTrigger {...props} />\n)\n\ntype CommandFilter = NonNullable<ComponentProps<typeof Command>[\"filter\"]>\n\n// cmdk's default filter is fuzzy, so \"opus\" also matches\n// \"anthropic/claude-sonnet-4\" through scattered letters. Model ids are short:\n// every whitespace-separated term must appear verbatim in the value or a keyword.\nconst modelSelectorFilter: CommandFilter = (value, search, keywords = []) => {\n  const terms = search.toLowerCase().split(/\\s+/).filter(Boolean)\n  if (terms.length === 0) {\n    return 1\n  }\n  const haystack = [value, ...keywords].join(\" \").toLowerCase()\n  return terms.every((term) => haystack.includes(term)) ? 1 : 0\n}\n\nexport type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {\n  title?: ReactNode\n  /** Replaces the whole-term filter; `defaultFilter` from \"cmdk\" restores fuzzy matching. */\n  filter?: CommandFilter | undefined\n  /** `false` turns cmdk filtering off so the consumer renders only the matching items. */\n  shouldFilter?: boolean | undefined\n}\n\nexport const ModelSelectorContent = ({\n  className,\n  children,\n  title = \"Model Selector\",\n  filter = modelSelectorFilter,\n  shouldFilter = true,\n  ...props\n}: ModelSelectorContentProps) => (\n  <DialogContent\n    aria-describedby={undefined}\n    className={cn(\n      \"border-none! p-0 outline! outline-border! outline-solid!\",\n      className\n    )}\n    {...props}\n  >\n    <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n    <Command\n      className=\"**:data-[slot=command-input-wrapper]:h-auto **:data-[slot=input-group]:h-auto!\"\n      filter={filter}\n      shouldFilter={shouldFilter}\n    >\n      {children}\n    </Command>\n  </DialogContent>\n)\n\nexport type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>\n\nexport const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (\n  <CommandDialog {...props} />\n)\n\nexport type ModelSelectorInputProps = ComponentProps<typeof CommandInput>\n\nexport const ModelSelectorInput = ({\n  className,\n  ...props\n}: ModelSelectorInputProps) => (\n  <CommandInput className={cn(\"h-auto py-3.5\", className)} {...props} />\n)\n\nconst EmptySlotContext = createContext<HTMLElement | null>(null)\n\nexport type ModelSelectorListProps = ComponentProps<typeof CommandList>\n\n// cmdk renders its empty state inside the listbox, where a message is not a\n// permitted child (axe `aria-required-children`). The list owns a live region\n// after itself and ModelSelectorEmpty portals into it, so the composition can\n// stay the same as upstream and the message is announced.\nexport const ModelSelectorList = ({\n  children,\n  ...props\n}: ModelSelectorListProps) => {\n  const [slot, setSlot] = useState<HTMLElement | null>(null)\n\n  return (\n    <EmptySlotContext.Provider value={slot}>\n      <CommandList {...props}>{children}</CommandList>\n      <div aria-live=\"polite\" data-slot=\"model-selector-empty\" ref={setSlot} />\n    </EmptySlotContext.Provider>\n  )\n}\n\nexport type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>\n\nexport const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => {\n  const slot = useContext(EmptySlotContext)\n  const empty = <CommandEmpty {...props} />\n\n  return slot ? createPortal(empty, slot) : empty\n}\n\nexport type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>\n\nexport const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (\n  <CommandGroup {...props} />\n)\n\nexport type ModelSelectorItemProps = ComponentProps<typeof CommandItem>\n\nexport const ModelSelectorItem = (props: ModelSelectorItemProps) => (\n  <CommandItem {...props} />\n)\n\nexport type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>\n\nexport const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (\n  <CommandShortcut {...props} />\n)\n\nexport type ModelSelectorSeparatorProps = ComponentProps<\n  typeof CommandSeparator\n>\n\n// cmdk forces role=\"separator\", which a listbox may not own (axe\n// `aria-required-children`). The divider is decorative: the groups around it\n// carry their own headings, so it is hidden from assistive technology.\nexport const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (\n  <div aria-hidden=\"true\">\n    <CommandSeparator {...props} />\n  </div>\n)\n\nexport type ModelSelectorLogoProps = Omit<\n  ComponentProps<\"img\">,\n  \"src\" | \"alt\"\n> & {\n  provider:\n    | \"moonshotai-cn\"\n    | \"lucidquery\"\n    | \"moonshotai\"\n    | \"zai-coding-plan\"\n    | \"alibaba\"\n    | \"xai\"\n    | \"vultr\"\n    | \"nvidia\"\n    | \"upstage\"\n    | \"groq\"\n    | \"github-copilot\"\n    | \"mistral\"\n    | \"vercel\"\n    | \"nebius\"\n    | \"deepseek\"\n    | \"alibaba-cn\"\n    | \"google-vertex-anthropic\"\n    | \"venice\"\n    | \"chutes\"\n    | \"cortecs\"\n    | \"github-models\"\n    | \"togetherai\"\n    | \"azure\"\n    | \"baseten\"\n    | \"huggingface\"\n    | \"opencode\"\n    | \"fastrouter\"\n    | \"google\"\n    | \"google-vertex\"\n    | \"cloudflare-workers-ai\"\n    | \"inception\"\n    | \"wandb\"\n    | \"openai\"\n    | \"zhipuai-coding-plan\"\n    | \"perplexity\"\n    | \"openrouter\"\n    | \"zenmux\"\n    | \"v0\"\n    | \"iflowcn\"\n    | \"synthetic\"\n    | \"deepinfra\"\n    | \"zhipuai\"\n    | \"submodel\"\n    | \"zai\"\n    | \"inference\"\n    | \"requesty\"\n    | \"morph\"\n    | \"lmstudio\"\n    | \"anthropic\"\n    | \"aihubmix\"\n    | \"fireworks-ai\"\n    | \"modelscope\"\n    | \"llama\"\n    | \"scaleway\"\n    | \"amazon-bedrock\"\n    | \"cerebras\"\n    | (string & {})\n  /** Empty by default: the logo is decorative next to ModelSelectorName. */\n  alt?: string | undefined\n  /**\n   * Replaces the default `https://models.dev/logos/<provider>.svg`: a\n   * self-hosted file or a data URI for offline use or an `img-src 'self'` CSP.\n   */\n  src?: string | undefined\n}\n\nexport const ModelSelectorLogo = ({\n  provider,\n  className,\n  alt = \"\",\n  src,\n  onError,\n  ...props\n}: ModelSelectorLogoProps) => {\n  const source = src ?? `https://models.dev/logos/${provider}.svg`\n  // models.dev has no logo for an unknown provider; hide the broken image\n  // instead of showing the browser's placeholder glyph.\n  const [failedSource, setFailedSource] = useState<string | null>(null)\n\n  return (\n    // biome-ignore lint/performance/noImgElement: remote SVG logos from models.dev, not an optimizable asset\n    <img\n      aria-hidden={alt === \"\" || undefined}\n      hidden={failedSource === source || undefined}\n      {...props}\n      alt={alt}\n      className={cn(\"size-3 dark:invert\", className)}\n      height={12}\n      onError={(event) => {\n        setFailedSource(source)\n        onError?.(event)\n      }}\n      src={source}\n      width={12}\n    />\n  )\n}\n\nexport type ModelSelectorLogoGroupProps = ComponentProps<\"div\">\n\nexport const ModelSelectorLogoGroup = ({\n  className,\n  ...props\n}: ModelSelectorLogoGroupProps) => (\n  <div\n    className={cn(\n      \"flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground\",\n      className\n    )}\n    {...props}\n  />\n)\n\nexport type ModelSelectorNameProps = ComponentProps<\"span\">\n\nexport const ModelSelectorName = ({\n  className,\n  ...props\n}: ModelSelectorNameProps) => (\n  <span className={cn(\"flex-1 truncate text-left\", className)} {...props} />\n)\n",
      "type": "registry:component",
      "target": "components/ai/model-selector.tsx"
    }
  ],
  "docs": "Derived from Vercel AI Elements (Apache-2.0, Copyright 2023 Vercel, Inc.); ported to Base UI and maintained by uifiles. Built for the Base UI styles (base-nova): it composes the Base UI wrappers from components/ui and relies on their render prop and event signatures, so it does not work on Radix or React Aria styles. Base UI Dialog: ModelSelectorTrigger takes render={<Button />} instead of asChild, and ModelSelector's onOpenChange receives (open, eventDetails). ModelSelectorContent is the Base UI Dialog Popup, so initialFocus/finalFocus replace onOpenAutoFocus/onCloseAutoFocus. ModelSelectorContent filters by whole terms (every whitespace-separated term of the query must appear in the item value or its keywords, case-insensitive) instead of cmdk's fuzzy match; pass filter (for example defaultFilter from \"cmdk\") or shouldFilter={false} to change that, and give ModelSelectorItem keywords for names that differ from the value. ModelSelectorEmpty placed inside ModelSelectorList renders after the listbox in a polite live region, and ModelSelectorSeparator is decorative (aria-hidden), because a listbox may not own a message or a separator. ModelSelectorLogo is decorative (alt=\"\", aria-hidden) unless you pass alt; by default it loads https://models.dev/logos/<provider>.svg at runtime (a third-party request from the user's browser, blocked by an img-src 'self' CSP or offline) and hides itself when the image fails to load (onError still fires); pass src to serve a self-hosted file or a data URI instead, as the docs preview does with inline placeholder marks so it makes no external request. ModelSelectorDialog wraps the base-nova CommandDialog (title, description, className, showCloseButton). The base-nova command input is an InputGroup with a forced h-8, so ModelSelectorContent also sets **:data-[slot=input-group]:h-auto! to keep upstream's taller py-3.5 input. The file is a client module (\"use client\").",
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:component"
}