{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-picker",
  "title": "Model Picker",
  "description": "A compact model menu with descriptions, plan badges, effort controls, and additional models.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0"
  ],
  "registryDependencies": [
    "button",
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/model-picker.tsx",
      "content": "import { Menu } from '@base-ui/react/menu'\nimport { CheckReadIcon as CheckIcon } from '@solar-icons/react/linear/check-read'\nimport { AltArrowDownIcon as ChevronDownIcon } from '@solar-icons/react/linear/alt-arrow-down'\nimport { AltArrowRightIcon as ChevronRightIcon } from '@solar-icons/react/linear/alt-arrow-right'\nimport { useState, type ReactNode } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from 'cn'\n\ninterface ModelOption {\n  description?: string\n  provider?: 'openai' | 'claude'\n  family?: string\n  id: string\n  name: string\n  price?: string\n  badge?: string\n  requiresUpgrade?: boolean\n}\n\ntype ModelEffort = 'low' | 'medium' | 'high' | 'extra'\n\ninterface ModelPickerProps {\n  className?: string\n  defaultValue?: string\n  models: ModelOption[]\n  moreModels?: ModelOption[]\n  onValueChange?: (value: string) => void\n  value?: string\n  effort?: ModelEffort\n  defaultEffort?: ModelEffort\n  onEffortChange?: (effort: ModelEffort) => void\n  onUpgrade?: (modelId: string) => void\n  footer?: ReactNode\n}\n\nconst surface =\n  'w-80 max-h-[var(--available-height)] max-w-[calc(100vw-2rem)] overflow-y-auto rounded-[16px] bg-card p-1.5 font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card outline-none origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0 motion-reduce:transition-opacity'\nconst row =\n  'flex w-full cursor-pointer items-center gap-3 rounded-[10px] px-3 py-2 text-left outline-none data-highlighted:bg-muted data-disabled:cursor-default data-disabled:opacity-50'\nconst effortLabels: Record<ModelEffort, string> = {\n  low: 'Low',\n  medium: 'Medium',\n  high: 'High',\n  extra: 'Extra',\n}\n\nfunction ModelProviderIcon({ provider }: { provider?: ModelOption['provider'] }) {\n  if (!provider) return null\n  if (provider === 'claude')\n    return <img src=\"/icons/providers/claude.svg\" alt=\"\" className=\"size-4 shrink-0\" />\n  return (\n    <span aria-hidden=\"true\" className=\"size-4 shrink-0\">\n      <img src=\"/icons/providers/openai.svg\" alt=\"\" className=\"size-4 scale-150 dark:hidden\" />\n      <img\n        src=\"/icons/providers/openai-dark.svg\"\n        alt=\"\"\n        className=\"hidden size-4 scale-150 dark:block\"\n      />\n    </span>\n  )\n}\n\nfunction ModelPicker({\n  className,\n  defaultValue,\n  models,\n  moreModels = [],\n  onValueChange,\n  value,\n  effort,\n  defaultEffort = 'medium',\n  onEffortChange,\n  onUpgrade,\n  footer,\n}: ModelPickerProps) {\n  const [internalValue, setInternalValue] = useState(\n    defaultValue ?? models.find((model) => !model.requiresUpgrade)?.id ?? '',\n  )\n  const [internalEffort, setInternalEffort] = useState(defaultEffort)\n  const selectedValue = value ?? internalValue\n  const selectedEffort = effort ?? internalEffort\n  const selectedModel = [...models, ...moreModels].find((model) => model.id === selectedValue)\n\n  function selectModel(id: string) {\n    if (value === undefined) setInternalValue(id)\n    onValueChange?.(id)\n  }\n\n  function modelRows(items: ModelOption[]) {\n    return items.map((model) => {\n      const content = (\n        <>\n          <ModelProviderIcon provider={model.provider} />\n          <span className=\"min-w-0 flex-1\">\n            <span className=\"flex flex-wrap items-center gap-2 text-sm\">\n              {model.name}\n              {(model.badge || model.family) && (\n                <span className=\"rounded-sm bg-muted px-1.5 text-xs leading-5\">\n                  {model.badge ?? model.family}\n                </span>\n              )}\n            </span>\n            {model.description && (\n              <span className=\"mt-0.5 block text-xs leading-5\">{model.description}</span>\n            )}\n          </span>\n        </>\n      )\n      return model.requiresUpgrade ? (\n        <Menu.Item\n          className={row}\n          key={model.id}\n          disabled={!onUpgrade}\n          onClick={() => onUpgrade?.(model.id)}\n        >\n          {content}\n          <span className=\"shrink-0 rounded-full bg-primary bg-linear-to-b from-[color-mix(in_oklch,var(--primary),var(--primary-foreground)_30%)] to-primary px-2.5 py-1 text-xs leading-4 text-primary-foreground dark:bg-none\">\n            Upgrade\n          </span>\n        </Menu.Item>\n      ) : (\n        <Menu.RadioItem className={row} key={model.id} value={model.id} closeOnClick>\n          {content}\n          {model.price && <span className=\"text-xs\">{model.price}</span>}\n          <Menu.RadioItemIndicator className=\"ml-auto shrink-0\">\n            <CheckIcon className=\"size-4\" />\n          </Menu.RadioItemIndicator>\n        </Menu.RadioItem>\n      )\n    })\n  }\n\n  return (\n    <Menu.Root>\n      <Menu.Trigger\n        render={\n          <Button\n            className={cn(\n              'h-8 gap-2 rounded-full px-3 text-sm font-[450] tracking-[-0.05px] text-muted-foreground',\n              className,\n            )}\n            variant=\"ghost\"\n          />\n        }\n      >\n        <ModelProviderIcon provider={selectedModel?.provider} />\n        <span className=\"max-w-40 truncate\">{selectedModel?.name ?? 'Select model'}</span>\n        <span className=\"text-muted-foreground/80\">{effortLabels[selectedEffort]}</span>\n        <ChevronDownIcon className=\"size-3.5 transition-[rotate] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] in-data-popup-open:rotate-180\" />\n      </Menu.Trigger>\n      <Menu.Portal>\n        <Menu.Positioner side=\"bottom\" align=\"end\" sideOffset={8} className=\"z-50\">\n          <Menu.Popup className={surface}>\n            <Menu.RadioGroup value={selectedValue} onValueChange={selectModel}>\n              {modelRows(models)}\n            </Menu.RadioGroup>\n            <Menu.Separator className=\"mx-3 my-1 h-px bg-border/60\" />\n            <Menu.SubmenuRoot>\n              <Menu.SubmenuTrigger className={row}>\n                Effort <span className=\"ml-auto text-xs\">{effortLabels[selectedEffort]}</span>\n                <ChevronRightIcon className=\"size-3.5\" />\n              </Menu.SubmenuTrigger>\n              <Menu.Portal>\n                <Menu.Positioner side=\"right\" align=\"start\" sideOffset={6} className=\"z-50\">\n                  <Menu.Popup className={cn(surface, 'w-64')}>\n                    <p className=\"px-3 py-2 text-xs leading-5\">\n                      Higher effort gives the model more time to work through complex tasks.\n                    </p>\n                    <Menu.RadioGroup\n                      value={selectedEffort}\n                      onValueChange={(next: ModelEffort) => {\n                        if (effort === undefined) setInternalEffort(next)\n                        onEffortChange?.(next)\n                      }}\n                    >\n                      {(Object.keys(effortLabels) as ModelEffort[]).map((level) => (\n                        <Menu.RadioItem className={row} key={level} value={level}>\n                          {effortLabels[level]}\n                          <Menu.RadioItemIndicator className=\"ml-auto\">\n                            <CheckIcon className=\"size-4\" />\n                          </Menu.RadioItemIndicator>\n                        </Menu.RadioItem>\n                      ))}\n                    </Menu.RadioGroup>\n                  </Menu.Popup>\n                </Menu.Positioner>\n              </Menu.Portal>\n            </Menu.SubmenuRoot>\n            {moreModels.length > 0 && (\n              <>\n                <Menu.Separator className=\"mx-3 my-1 h-px bg-border/60\" />\n                <Menu.SubmenuRoot>\n                  <Menu.SubmenuTrigger className={row}>\n                    More models\n                    <ChevronRightIcon className=\"ml-auto size-3.5\" />\n                  </Menu.SubmenuTrigger>\n                  <Menu.Portal>\n                    <Menu.Positioner side=\"right\" align=\"start\" sideOffset={6} className=\"z-50\">\n                      <Menu.Popup className={surface}>\n                        <Menu.RadioGroup value={selectedValue} onValueChange={selectModel}>\n                          {modelRows(moreModels)}\n                        </Menu.RadioGroup>\n                      </Menu.Popup>\n                    </Menu.Positioner>\n                  </Menu.Portal>\n                </Menu.SubmenuRoot>\n              </>\n            )}\n            {footer && (\n              <>\n                <Menu.Separator className=\"mx-3 my-1 h-px bg-border/60\" />\n                <div className=\"px-3 py-2 text-xs leading-5\">{footer}</div>\n              </>\n            )}\n          </Menu.Popup>\n        </Menu.Positioner>\n      </Menu.Portal>\n    </Menu.Root>\n  )\n}\n\nexport { ModelPicker }\nexport type { ModelOption, ModelPickerProps, ModelEffort }\n",
      "type": "registry:component",
      "target": "@components/ai/model-picker.tsx"
    }
  ],
  "type": "registry:component"
}