{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-selector",
  "type": "registry:component",
  "title": "Model Selector",
  "description": "Dropdown selector for choosing AI models.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/model-selector/model-selector.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useRef, useState } from \"react\";\n\nimport { ArrowUpDown, Filter } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandList,\n} from \"@vllnt/ui\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@vllnt/ui\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@vllnt/ui\";\nimport { Input } from \"@vllnt/ui\";\n\n/** Model information for the selector. */\nexport type ModelInfo = {\n  description?: string;\n  id: string;\n  name: string;\n  pricing?: {\n    input?: number; // per 1M tokens\n    output?: number; // per 1M tokens\n  };\n};\n\nexport type ModelSelectorProps = {\n  models: ModelInfo[];\n  onOpenChange: (open: boolean) => void;\n  onSelectModel: (modelId: string) => void;\n  open: boolean;\n  selectedModelId: string;\n};\n\ntype SortOption = \"name\" | \"price-high\" | \"price-low\" | \"provider\";\ntype SelectionGuardReference = { current: null | string };\n\nfunction getProvider(modelId: string): string {\n  const parts = modelId.split(\"/\");\n  return parts[0] || \"unknown\";\n}\n\nfunction getAveragePrice(model: ModelInfo): number {\n  if (!model.pricing) return Number.POSITIVE_INFINITY;\n  const inputPrice = model.pricing.input ?? 0;\n  const outputPrice = model.pricing.output ?? inputPrice;\n  return (inputPrice + outputPrice) / 2;\n}\n\nfunction formatPrice(price?: number): string {\n  if (!price) return \"N/A\";\n  return `$${price.toFixed(2)}/1M`;\n}\n\nfunction getProviders(models: ModelInfo[]): string[] {\n  const { providers } = models.reduce<{\n    providers: string[];\n    seen: Set<string>;\n  }>(\n    (accumulator, model) => {\n      const provider = getProvider(model.id);\n      if (!accumulator.seen.has(provider)) {\n        accumulator.seen.add(provider);\n        accumulator.providers.push(provider);\n      }\n      return accumulator;\n    },\n    { providers: [], seen: new Set<string>() },\n  );\n  return providers.sort();\n}\n\nfunction applyProviderFilter(models: ModelInfo[], providerFilter: string) {\n  if (providerFilter === \"all\") return models;\n  return models.filter((model) => getProvider(model.id) === providerFilter);\n}\n\nfunction applySearchFilter(models: ModelInfo[], modelSearchQuery: string) {\n  if (!modelSearchQuery.trim()) return models;\n  const query = modelSearchQuery.toLowerCase();\n  return models.filter(\n    (model) =>\n      model.name.toLowerCase().includes(query) ||\n      model.id.toLowerCase().includes(query) ||\n      model.description?.toLowerCase().includes(query) ||\n      getProvider(model.id).toLowerCase().includes(query),\n  );\n}\n\nfunction sortModelsBy(models: ModelInfo[], sortBy: SortOption) {\n  const sorted = [...models];\n  return sorted.sort((a, b) => {\n    switch (sortBy) {\n      case \"name\":\n        return a.name.localeCompare(b.name);\n      case \"price-low\":\n        return getAveragePrice(a) - getAveragePrice(b);\n      case \"price-high\":\n        return getAveragePrice(b) - getAveragePrice(a);\n      case \"provider\": {\n        const providerA = getProvider(a.id);\n        const providerB = getProvider(b.id);\n        if (providerA !== providerB) return providerA.localeCompare(providerB);\n        return a.name.localeCompare(b.name);\n      }\n      default:\n        return 0;\n    }\n  });\n}\n\nfunction promoteSelected(models: ModelInfo[], selectedModelId: string) {\n  const sorted = [...models];\n  const selectedIndex = sorted.findIndex(\n    (model) => model.id === selectedModelId,\n  );\n  if (selectedIndex > 0) {\n    const [selected] = sorted.splice(selectedIndex, 1);\n    if (selected) sorted.unshift(selected);\n  }\n  return sorted;\n}\n\nfunction resetFilters(\n  setModelSearchQuery: (value: string) => void,\n  setProviderFilter: (value: string) => void,\n  setSortBy: (value: SortOption) => void,\n) {\n  setModelSearchQuery(\"\");\n  setProviderFilter(\"all\");\n  setSortBy(\"name\");\n}\n\ntype HandleModelSelectArguments = {\n  handleClose: (nextOpen: boolean) => void;\n  id: string;\n  onSelectModel: (id: string) => void;\n  selectionGuardReference: SelectionGuardReference;\n};\n\nfunction handleModelSelect({\n  handleClose,\n  id,\n  onSelectModel,\n  selectionGuardReference,\n}: HandleModelSelectArguments) {\n  if (selectionGuardReference.current === id) return;\n  selectionGuardReference.current = id;\n  onSelectModel(id);\n  setTimeout(() => {\n    if (selectionGuardReference.current === id)\n      selectionGuardReference.current = null;\n  }, 0);\n  handleClose(false);\n}\n\ntype FilterAndSortArguments = {\n  models: ModelInfo[];\n  modelSearchQuery: string;\n  providerFilter: string;\n  selectedModelId: string;\n  sortBy: SortOption;\n};\n\nfunction filterAndSortModels({\n  models,\n  modelSearchQuery,\n  providerFilter,\n  selectedModelId,\n  sortBy,\n}: FilterAndSortArguments): ModelInfo[] {\n  const filtered = applySearchFilter(\n    applyProviderFilter(models, providerFilter),\n    modelSearchQuery,\n  );\n  return promoteSelected(sortModelsBy(filtered, sortBy), selectedModelId);\n}\n\nfunction useFilteredModels(options: FilterAndSortArguments) {\n  const { models, modelSearchQuery, providerFilter, selectedModelId, sortBy } =\n    options;\n  return useMemo(\n    () =>\n      filterAndSortModels({\n        models,\n        modelSearchQuery,\n        providerFilter,\n        selectedModelId,\n        sortBy,\n      }),\n    [models, modelSearchQuery, sortBy, providerFilter, selectedModelId],\n  );\n}\n\ntype ModelSelectorState = {\n  filteredAndSortedModels: ModelInfo[];\n  handleClose: (nextOpen: boolean) => void;\n  handleSelect: (id: string) => void;\n  modelSearchQuery: string;\n  providerFilter: string;\n  providers: string[];\n  setModelSearchQuery: (value: string) => void;\n  setProviderFilter: (value: string) => void;\n  setSortBy: (value: SortOption) => void;\n  sortBy: SortOption;\n};\n\nfunction useModelSelectorState({\n  models,\n  onOpenChange,\n  onSelectModel,\n  selectedModelId,\n}: ModelSelectorProps): ModelSelectorState {\n  const [modelSearchQuery, setModelSearchQuery] = useState(\"\");\n  const [sortBy, setSortBy] = useState<SortOption>(\"name\");\n  const [providerFilter, setProviderFilter] = useState<string>(\"all\");\n  const selectionGuardReference = useRef<null | string>(null);\n  const providers = useMemo(() => getProviders(models), [models]);\n  const filteredAndSortedModels = useFilteredModels({\n    models,\n    modelSearchQuery,\n    providerFilter,\n    selectedModelId,\n    sortBy,\n  });\n  const handleClose = (nextOpen: boolean) => {\n    onOpenChange(nextOpen);\n    if (!nextOpen) {\n      resetFilters(setModelSearchQuery, setProviderFilter, setSortBy);\n    }\n  };\n  const handleSelect = (id: string) => {\n    handleModelSelect({\n      handleClose,\n      id,\n      onSelectModel,\n      selectionGuardReference,\n    });\n  };\n  return {\n    filteredAndSortedModels,\n    handleClose,\n    handleSelect,\n    modelSearchQuery,\n    providerFilter,\n    providers,\n    setModelSearchQuery,\n    setProviderFilter,\n    setSortBy,\n    sortBy,\n  };\n}\n\ntype ProviderFilterMenuProps = {\n  onChange: (value: string) => void;\n  providerFilter: string;\n  providers: string[];\n};\n\nfunction ProviderFilterMenu({\n  onChange,\n  providerFilter,\n  providers,\n}: ProviderFilterMenuProps) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button className=\"h-9 gap-2\" size=\"sm\" variant=\"outline\">\n          <Filter className=\"size-4\" />\n          {providerFilter === \"all\" ? \"All Providers\" : providerFilter}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-48\">\n        <DropdownMenuLabel>Filter by Provider</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup onValueChange={onChange} value={providerFilter}>\n          <DropdownMenuRadioItem value=\"all\">\n            All Providers\n          </DropdownMenuRadioItem>\n          {providers.map((provider) => (\n            <DropdownMenuRadioItem key={provider} value={provider}>\n              {provider}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\ntype SortMenuProps = {\n  onChange: (value: SortOption) => void;\n  sortBy: SortOption;\n};\n\nfunction SortMenu({ onChange, sortBy }: SortMenuProps) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button className=\"h-9 gap-2\" size=\"sm\" variant=\"outline\">\n          <ArrowUpDown className=\"size-4\" />\n          Sort\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-48\">\n        <DropdownMenuLabel>Sort by</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup\n          onValueChange={(value) => {\n            onChange(value as SortOption);\n          }}\n          value={sortBy}\n        >\n          <DropdownMenuRadioItem value=\"name\">Name</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"provider\">\n            Provider\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"price-low\">\n            Price: Low to High\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"price-high\">\n            Price: High to Low\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\ntype ModelSelectorFiltersProps = {\n  modelSearchQuery: string;\n  onProviderChange: (value: string) => void;\n  onSearchChange: (value: string) => void;\n  onSortChange: (value: SortOption) => void;\n  providerFilter: string;\n  providers: string[];\n  sortBy: SortOption;\n};\n\nfunction ModelSelectorFilters({\n  modelSearchQuery,\n  onProviderChange,\n  onSearchChange,\n  onSortChange,\n  providerFilter,\n  providers,\n  sortBy,\n}: ModelSelectorFiltersProps) {\n  return (\n    <div className=\"flex items-center gap-2 px-1 pb-2 border-b\">\n      <div className=\"flex-1\">\n        <Input\n          className=\"h-9\"\n          onChange={(event) => {\n            onSearchChange(event.target.value);\n          }}\n          placeholder=\"Search models or providers...\"\n          value={modelSearchQuery}\n        />\n      </div>\n      <ProviderFilterMenu\n        onChange={onProviderChange}\n        providerFilter={providerFilter}\n        providers={providers}\n      />\n      <SortMenu onChange={onSortChange} sortBy={sortBy} />\n    </div>\n  );\n}\n\ntype ModelListProps = {\n  models: ModelInfo[];\n  onSelect: (id: string) => void;\n  selectedModelId: string;\n};\n\nfunction ModelList({ models, onSelect, selectedModelId }: ModelListProps) {\n  return (\n    <Command className=\"flex-1\" shouldFilter={false}>\n      <CommandList className=\"max-h-[60vh]\">\n        <CommandEmpty>No models found.</CommandEmpty>\n        <CommandGroup>\n          {models.map((model) => (\n            <ModelListItem\n              key={model.id}\n              model={model}\n              onSelect={onSelect}\n              selectedModelId={selectedModelId}\n            />\n          ))}\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  );\n}\n\ntype ModelPricingProps = {\n  pricing?: ModelInfo[\"pricing\"];\n};\n\nfunction ModelPricing({ pricing }: ModelPricingProps) {\n  if (!pricing) return null;\n\n  return (\n    <div className=\"flex items-center gap-3 text-xs text-muted-foreground pointer-events-none\">\n      {pricing.input ? <span>In: {formatPrice(pricing.input)}</span> : null}\n      {pricing.output ? <span>Out: {formatPrice(pricing.output)}</span> : null}\n    </div>\n  );\n}\n\ntype ModelListItemProps = {\n  model: ModelInfo;\n  onSelect: (id: string) => void;\n  selectedModelId: string;\n};\n\nfunction ModelListItem({\n  model,\n  onSelect,\n  selectedModelId,\n}: ModelListItemProps) {\n  const isSelected = selectedModelId === model.id;\n  const provider = getProvider(model.id);\n\n  return (\n    <CommandItem\n      className={cn(\n        \"flex flex-col items-start py-3\",\n        isSelected && \"bg-accent\",\n      )}\n      disabled={isSelected}\n      onSelect={() => {\n        onSelect(model.id);\n      }}\n      value={model.id}\n    >\n      <div className=\"flex items-center justify-between w-full pointer-events-none\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"font-medium\">{model.name}</span>\n          {isSelected ? (\n            <Badge className=\"text-xs pointer-events-none\" variant=\"secondary\">\n              Selected\n            </Badge>\n          ) : null}\n          <Badge\n            className=\"text-xs font-mono pointer-events-none\"\n            variant=\"outline\"\n          >\n            {provider}\n          </Badge>\n        </div>\n        <ModelPricing pricing={model.pricing} />\n      </div>\n      {model.description ? (\n        <span className=\"text-xs text-muted-foreground mt-1 pointer-events-none\">\n          {model.description}\n        </span>\n      ) : null}\n      <span className=\"text-xs text-muted-foreground mt-1 font-mono pointer-events-none\">\n        {model.id}\n      </span>\n    </CommandItem>\n  );\n}\n\n/** Model selector dialog with search, filtering, and sorting. */\nexport function ModelSelector(props: ModelSelectorProps) {\n  const {\n    filteredAndSortedModels,\n    handleClose,\n    handleSelect,\n    modelSearchQuery,\n    providerFilter,\n    providers,\n    setModelSearchQuery,\n    setProviderFilter,\n    setSortBy,\n    sortBy,\n  } = useModelSelectorState(props);\n\n  return (\n    <Dialog onOpenChange={handleClose} open={props.open}>\n      <DialogContent className=\"max-w-2xl max-h-[80vh] flex flex-col\">\n        <DialogHeader>\n          <DialogTitle>Select Model</DialogTitle>\n          <DialogDescription className=\"sr-only\">\n            Search, filter, and select an AI model\n          </DialogDescription>\n        </DialogHeader>\n        <ModelSelectorFilters\n          modelSearchQuery={modelSearchQuery}\n          onProviderChange={setProviderFilter}\n          onSearchChange={setModelSearchQuery}\n          onSortChange={setSortBy}\n          providerFilter={providerFilter}\n          providers={providers}\n          sortBy={sortBy}\n        />\n        <ModelList\n          models={filteredAndSortedModels}\n          onSelect={handleSelect}\n          selectedModelId={props.selectedModelId}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
