{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-model-selector",
  "title": "AI Model Selector",
  "description": "Select AI models from different providers with search.",
  "registryDependencies": [
    "button",
    "command-menu",
    "empty",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-model-selector.tsx",
      "content": "\"use client\";\n\nimport {\n  Check,\n  ChevronDown,\n  Gauge,\n  Image as ImageIcon,\n  Rocket,\n  Search,\n  Sparkles,\n  Zap,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  CommandMenu,\n  CommandMenuContent,\n  CommandMenuInput,\n  CommandMenuItem,\n  CommandMenuList,\n  CommandMenuSeparator,\n  CommandMenuTrigger,\n  useCommandMenu,\n  useCommandMenuShortcut,\n} from \"@/registry/new-york/ui/command-menu\";\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/new-york/ui/empty\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/registry/new-york/ui/tooltip\";\n\nexport type AIProvider = \"openai\" | \"anthropic\" | \"google\" | \"meta\" | \"mistral\";\n\nexport interface AIModel {\n  id: string;\n  name: string;\n  provider: AIProvider;\n  description?: string;\n  features?: ModelFeature[];\n  isNew?: boolean;\n  isPreview?: boolean;\n}\n\nexport type ModelFeature =\n  | \"fast\"\n  | \"turbo\"\n  | \"reasoning\"\n  | \"multimodal\"\n  | \"long-context\";\n\ninterface ProviderConfig {\n  name: string;\n  icon: React.ReactNode;\n  ariaLabel: string;\n}\n\nconst PROVIDER_CONFIGS: Record<AIProvider, ProviderConfig> = {\n  openai: {\n    name: \"OpenAI\",\n    ariaLabel: \"OpenAI provider\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        viewBox=\"0 0 512 512\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M196.4 185.8v-48.6c0-4.1 1.5-7.2 5.1-9.2l97.8-56.3c13.3-7.7 29.2-11.3 45.6-11.3c61.4 0 100.4 47.6 100.4 98.3c0 3.6 0 7.7-.5 11.8l-101.5-59.4c-6.1-3.6-12.3-3.6-18.4 0zm228.3 189.4V259c0-7.2-3.1-12.3-9.2-15.9L287 168.4l42-24.1c3.6-2 6.7-2 10.2 0l97.8 56.4c28.2 16.4 47.1 51.2 47.1 85c0 38.9-23 74.8-59.4 89.6zM166.2 272.8l-42-24.6c-3.6-2-5.1-5.1-5.1-9.2V126.4c0-54.8 42-96.3 98.8-96.3c21.5 0 41.5 7.2 58.4 20l-100.9 58.4c-6.1 3.6-9.2 8.7-9.2 15.9v148.5zm90.4 52.2l-60.2-33.8v-71.7l60.2-33.8l60.2 33.8v71.7zm38.7 155.7c-21.5 0-41.5-7.2-58.4-20l100.9-58.4c6.1-3.6 9.2-8.7 9.2-15.9V237.9l42.5 24.6c3.6 2 5.1 5.1 5.1 9.2v112.6c0 54.8-42.5 96.3-99.3 96.3zM173.8 366.5l-97.7-56.3C47.9 293.8 29 259 29 225.2c0-39.4 23.6-74.8 59.9-89.6v116.7c0 7.2 3.1 12.3 9.2 15.9l128 74.2l-42 24.1c-3.6 2-6.7 2-10.2 0zm-5.6 84c-57.9 0-100.4-43.5-100.4-97.3c0-4.1.5-8.2 1-12.3l100.9 58.4c6.1 3.6 12.3 3.6 18.4 0l128.5-74.2v48.6c0 4.1-1.5 7.2-5.1 9.2l-97.8 56.3c-13.3 7.7-29.2 11.3-45.6 11.3zm127 60.9c62 0 113.7-44 125.4-102.4c57.3-14.9 94.2-68.6 94.2-123.4c0-35.8-15.4-70.7-43-95.7c2.6-10.8 4.1-21.5 4.1-32.3c0-73.2-59.4-128-128-128c-13.8 0-27.1 2-40.4 6.7c-23-22.5-54.8-36.9-89.6-36.9c-62 0-113.7 44-125.4 102.4c-57.3 14.8-94.2 68.6-94.2 123.4c0 35.8 15.4 70.7 43 95.7c-2.6 10.8-4.1 21.5-4.1 32.3c0 73.2 59.4 128 128 128c13.8 0 27.1-2 40.4-6.7c23 22.5 54.8 36.9 89.6 36.9\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    ),\n  },\n  anthropic: {\n    name: \"Anthropic\",\n    ariaLabel: \"Anthropic provider\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    ),\n  },\n  google: {\n    name: \"Google\",\n    ariaLabel: \"Google provider\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M12 2a9.96 9.96 0 0 1 6.29 2.226a1 1 0 0 1 .04 1.52l-1.51 1.362a1 1 0 0 1-1.265.06a6 6 0 1 0 2.103 6.836l.001-.004h-3.66a1 1 0 0 1-.992-.883L13 13v-2a1 1 0 0 1 1-1h6.945a1 1 0 0 1 .994.89q.06.55.061 1.11c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    ),\n  },\n  meta: {\n    name: \"Meta\",\n    ariaLabel: \"Meta provider\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M12 10.174Q14.649 5.999 16.648 6c2 0 3.263 2.213 4 5.217c.704 2.869.5 6.783-2 6.783c-1.114 0-2.648-1.565-4.148-3.652a27.6 27.6 0 0 1-2.5-4.174m0 0Q9.351 5.999 7.352 6c-2 0-3.263 2.213-4 5.217c-.704 2.869-.5 6.783 2 6.783C6.466 18 8 16.435 9.5 14.348q1.5-2.087 2.5-4.174\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2\"\n        />\n      </svg>\n    ),\n  },\n  mistral: {\n    name: \"Mistral\",\n    ariaLabel: \"Mistral provider\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          d=\"M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    ),\n  },\n};\n\nconst FEATURE_ICONS: Record<ModelFeature, React.ReactNode> = {\n  fast: <Zap aria-hidden=\"true\" className=\"size-3\" />,\n  turbo: <Rocket aria-hidden=\"true\" className=\"size-3\" />,\n  reasoning: <Sparkles aria-hidden=\"true\" className=\"size-3\" />,\n  multimodal: <ImageIcon aria-hidden=\"true\" className=\"size-3\" />,\n  \"long-context\": <Gauge aria-hidden=\"true\" className=\"size-3\" />,\n};\n\nconst FEATURE_LABELS: Record<ModelFeature, string> = {\n  fast: \"Fast\",\n  turbo: \"Turbo\",\n  reasoning: \"Reasoning\",\n  multimodal: \"Multimodal\",\n  \"long-context\": \"Long context\",\n};\n\nconst DEFAULT_MODELS: AIModel[] = [\n  {\n    id: \"gpt-4o\",\n    name: \"GPT-4o\",\n    provider: \"openai\",\n    description: \"Most capable model\",\n    features: [\"fast\", \"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gpt-4o-mini\",\n    name: \"GPT-4o Mini\",\n    provider: \"openai\",\n    description: \"Fast and affordable\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"gpt-4o-2024-11-20\",\n    name: \"GPT-4o (2024-11-20)\",\n    provider: \"openai\",\n    description: \"Latest GPT-4o snapshot\",\n    features: [\"fast\", \"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"o1\",\n    name: \"o1\",\n    provider: \"openai\",\n    description: \"Advanced reasoning\",\n    features: [\"reasoning\"],\n  },\n  {\n    id: \"o1-preview\",\n    name: \"o1 Preview\",\n    provider: \"openai\",\n    description: \"Advanced reasoning preview\",\n    features: [\"reasoning\"],\n    isPreview: true,\n  },\n  {\n    id: \"o1-mini\",\n    name: \"o1 Mini\",\n    provider: \"openai\",\n    description: \"Fast reasoning\",\n    features: [\"fast\", \"reasoning\"],\n  },\n  {\n    id: \"o1-mini-preview\",\n    name: \"o1 Mini Preview\",\n    provider: \"openai\",\n    description: \"Fast reasoning preview\",\n    features: [\"fast\", \"reasoning\"],\n    isPreview: true,\n  },\n  {\n    id: \"gpt-4-turbo\",\n    name: \"GPT-4 Turbo\",\n    provider: \"openai\",\n    description: \"High performance model\",\n    features: [\"turbo\", \"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gpt-4-turbo-preview\",\n    name: \"GPT-4 Turbo Preview\",\n    provider: \"openai\",\n    description: \"Latest GPT-4 Turbo\",\n    features: [\"turbo\", \"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"gpt-4\",\n    name: \"GPT-4\",\n    provider: \"openai\",\n    description: \"Original GPT-4 model\",\n    features: [\"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gpt-4-32k\",\n    name: \"GPT-4 32K\",\n    provider: \"openai\",\n    description: \"GPT-4 with extended context\",\n    features: [\"long-context\"],\n  },\n  {\n    id: \"gpt-3.5-turbo\",\n    name: \"GPT-3.5 Turbo\",\n    provider: \"openai\",\n    description: \"Fast and efficient\",\n    features: [\"turbo\"],\n  },\n  {\n    id: \"gpt-3.5-turbo-16k\",\n    name: \"GPT-3.5 Turbo 16K\",\n    provider: \"openai\",\n    description: \"GPT-3.5 with extended context\",\n    features: [\"turbo\", \"long-context\"],\n  },\n  {\n    id: \"claude-4-opus\",\n    name: \"Claude 4 Opus\",\n    provider: \"anthropic\",\n    description: \"Most capable model\",\n    features: [\"reasoning\", \"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"claude-4-sonnet\",\n    name: \"Claude 4 Sonnet\",\n    provider: \"anthropic\",\n    description: \"Balanced performance\",\n    features: [\"fast\", \"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"claude-3.5-sonnet\",\n    name: \"Claude 3.5 Sonnet\",\n    provider: \"anthropic\",\n    description: \"Fast and intelligent\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"claude-3.5-sonnet-20241022\",\n    name: \"Claude 3.5 Sonnet (2024-10-22)\",\n    provider: \"anthropic\",\n    description: \"Latest Claude 3.5 Sonnet\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"claude-3.5-haiku\",\n    name: \"Claude 3.5 Haiku\",\n    provider: \"anthropic\",\n    description: \"Fastest Claude model\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"claude-3-opus\",\n    name: \"Claude 3 Opus\",\n    provider: \"anthropic\",\n    description: \"Most powerful model\",\n    features: [\"reasoning\", \"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"claude-3-sonnet\",\n    name: \"Claude 3 Sonnet\",\n    provider: \"anthropic\",\n    description: \"Balanced Claude 3\",\n    features: [\"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"claude-3-haiku\",\n    name: \"Claude 3 Haiku\",\n    provider: \"anthropic\",\n    description: \"Fast and cost-effective\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"claude-3-5-sonnet-20240620\",\n    name: \"Claude 3.5 Sonnet (2024-06-20)\",\n    provider: \"anthropic\",\n    description: \"Claude 3.5 Sonnet snapshot\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"gemini-2.0-pro\",\n    name: \"Gemini 2.0 Pro\",\n    provider: \"google\",\n    description: \"Advanced capabilities\",\n    features: [\"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"gemini-2.0-flash\",\n    name: \"Gemini 2.0 Flash\",\n    provider: \"google\",\n    description: \"Fast Gemini 2.0\",\n    features: [\"fast\", \"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"gemini-1.5-pro\",\n    name: \"Gemini 1.5 Pro\",\n    provider: \"google\",\n    description: \"Multimodal model\",\n    features: [\"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gemini-1.5-pro-latest\",\n    name: \"Gemini 1.5 Pro Latest\",\n    provider: \"google\",\n    description: \"Latest Gemini 1.5 Pro\",\n    features: [\"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gemini-1.5-flash\",\n    name: \"Gemini 1.5 Flash\",\n    provider: \"google\",\n    description: \"Fast and efficient\",\n    features: [\"fast\", \"multimodal\", \"long-context\"],\n  },\n  {\n    id: \"gemini-1.5-flash-8b\",\n    name: \"Gemini 1.5 Flash 8B\",\n    provider: \"google\",\n    description: \"Lightweight Flash model\",\n    features: [\"fast\", \"multimodal\"],\n  },\n  {\n    id: \"gemini-1.0-pro\",\n    name: \"Gemini 1.0 Pro\",\n    provider: \"google\",\n    description: \"Original Gemini Pro\",\n    features: [\"multimodal\"],\n  },\n  {\n    id: \"gemini-ultra\",\n    name: \"Gemini Ultra\",\n    provider: \"google\",\n    description: \"Most capable model\",\n    features: [\"reasoning\", \"multimodal\", \"long-context\"],\n    isPreview: true,\n  },\n  {\n    id: \"llama-3.1-405b\",\n    name: \"Llama 3.1 405B\",\n    provider: \"meta\",\n    description: \"Open source model\",\n    features: [\"long-context\"],\n  },\n  {\n    id: \"llama-3.1-70b\",\n    name: \"Llama 3.1 70B\",\n    provider: \"meta\",\n    description: \"Balanced performance\",\n    features: [\"fast\", \"long-context\"],\n  },\n  {\n    id: \"llama-3.1-8b\",\n    name: \"Llama 3.1 8B\",\n    provider: \"meta\",\n    description: \"Lightweight model\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"llama-3.2\",\n    name: \"Llama 3.2\",\n    provider: \"meta\",\n    description: \"Latest open source\",\n    features: [\"fast\"],\n    isNew: true,\n  },\n  {\n    id: \"llama-3.2-3b\",\n    name: \"Llama 3.2 3B\",\n    provider: \"meta\",\n    description: \"Ultra-lightweight\",\n    features: [\"fast\"],\n    isNew: true,\n  },\n  {\n    id: \"llama-3.2-1b\",\n    name: \"Llama 3.2 1B\",\n    provider: \"meta\",\n    description: \"Smallest Llama model\",\n    features: [\"fast\"],\n    isNew: true,\n  },\n  {\n    id: \"llama-3-70b\",\n    name: \"Llama 3 70B\",\n    provider: \"meta\",\n    description: \"Llama 3 base model\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"llama-3-8b\",\n    name: \"Llama 3 8B\",\n    provider: \"meta\",\n    description: \"Llama 3 lightweight\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"mistral-large\",\n    name: \"Mistral Large\",\n    provider: \"mistral\",\n    description: \"High performance\",\n    features: [\"fast\", \"long-context\"],\n  },\n  {\n    id: \"mistral-large-2407\",\n    name: \"Mistral Large 2407\",\n    provider: \"mistral\",\n    description: \"Mistral Large snapshot\",\n    features: [\"fast\", \"long-context\"],\n  },\n  {\n    id: \"mistral-large-2402\",\n    name: \"Mistral Large 2402\",\n    provider: \"mistral\",\n    description: \"Mistral Large snapshot\",\n    features: [\"fast\", \"long-context\"],\n  },\n  {\n    id: \"mistral-medium\",\n    name: \"Mistral Medium\",\n    provider: \"mistral\",\n    description: \"Balanced capabilities\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"mistral-small\",\n    name: \"Mistral Small\",\n    provider: \"mistral\",\n    description: \"Fast and efficient\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"mistral-small-2402\",\n    name: \"Mistral Small 2402\",\n    provider: \"mistral\",\n    description: \"Mistral Small snapshot\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"mistral-tiny\",\n    name: \"Mistral Tiny\",\n    provider: \"mistral\",\n    description: \"Ultra-fast model\",\n    features: [\"fast\"],\n  },\n  {\n    id: \"pixtral-large\",\n    name: \"Pixtral Large\",\n    provider: \"mistral\",\n    description: \"Multimodal vision model\",\n    features: [\"multimodal\", \"long-context\"],\n    isNew: true,\n  },\n  {\n    id: \"pixtral-12b\",\n    name: \"Pixtral 12B\",\n    provider: \"mistral\",\n    description: \"Efficient vision model\",\n    features: [\"fast\", \"multimodal\"],\n    isNew: true,\n  },\n];\n\nexport interface AIModelSelectorProps {\n  models?: AIModel[];\n  selectedModelId?: string;\n  onModelSelect?: (model: AIModel) => void;\n  trigger?: React.ReactNode;\n  className?: string;\n  isLoading?: boolean;\n}\n\ninterface ModelFeatureBadgeProps {\n  feature: ModelFeature;\n}\n\nfunction ModelFeatureBadge({ feature }: ModelFeatureBadgeProps) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <div\n          aria-label={FEATURE_LABELS[feature]}\n          className=\"flex min-h-[32px] min-w-[32px] cursor-help items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n          role=\"button\"\n          tabIndex={0}\n        >\n          {FEATURE_ICONS[feature]}\n        </div>\n      </TooltipTrigger>\n      <TooltipContent>\n        <p>{FEATURE_LABELS[feature]}</p>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\ninterface ModelBadgeProps {\n  label: string;\n  variant: \"new\" | \"preview\";\n}\n\nfunction ModelBadge({ label, variant }: ModelBadgeProps) {\n  return (\n    <span\n      className={cn(\n        \"shrink-0 rounded-full px-1.5 py-0.5 font-medium text-xs tabular-nums\",\n        variant === \"new\" &&\n          \"bg-primary/10 text-primary ring-1 ring-primary/20\",\n        variant === \"preview\" && \"bg-muted text-muted-foreground\"\n      )}\n    >\n      {label}\n    </span>\n  );\n}\n\ninterface ModelItemProps {\n  model: AIModel;\n  isSelected: boolean;\n  index: number;\n  onSelect: () => void;\n}\n\nfunction ModelItem({ model, isSelected, index, onSelect }: ModelItemProps) {\n  return (\n    <CommandMenuItem index={index} onSelect={onSelect}>\n      <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          <div className=\"flex items-center gap-2\">\n            <span className=\"font-medium text-sm tabular-nums\">\n              {model.name}\n            </span>\n            {model.isNew && <ModelBadge label=\"New\" variant=\"new\" />}\n            {model.isPreview && (\n              <ModelBadge label=\"Preview\" variant=\"preview\" />\n            )}\n          </div>\n          {model.description && (\n            <span className=\"text-muted-foreground text-xs\">\n              {model.description}\n            </span>\n          )}\n        </div>\n        <div className=\"flex shrink-0 items-center gap-1.5\">\n          {model.features?.map((feature) => (\n            <ModelFeatureBadge feature={feature} key={feature} />\n          ))}\n          {isSelected && (\n            <Check\n              aria-label=\"Selected\"\n              className=\"size-4 text-primary\"\n              role=\"status\"\n            />\n          )}\n        </div>\n      </div>\n    </CommandMenuItem>\n  );\n}\n\ninterface ProviderGroupHeaderProps {\n  provider: AIProvider;\n}\n\nfunction ProviderGroupHeader({ provider }: ProviderGroupHeaderProps) {\n  const config = PROVIDER_CONFIGS[provider];\n  return (\n    <div\n      aria-label={`${config.name} models`}\n      className=\"flex items-center gap-2 px-2 py-1.5\"\n      role=\"group\"\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"flex size-4 items-center justify-center text-muted-foreground\"\n      >\n        {config.icon}\n      </div>\n      <span className=\"font-semibold text-muted-foreground text-xs uppercase tabular-nums tracking-tight\">\n        {config.name}\n      </span>\n    </div>\n  );\n}\n\ninterface EmptyStateProps {\n  searchQuery: string;\n}\n\nfunction EmptyState({ searchQuery }: EmptyStateProps) {\n  return (\n    <Empty>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <Search aria-hidden=\"true\" className=\"size-6\" />\n        </EmptyMedia>\n        <EmptyTitle>\n          {searchQuery ? \"No models found\" : \"No models available\"}\n        </EmptyTitle>\n        <EmptyDescription>\n          {searchQuery\n            ? `No models match \"${searchQuery}\". Try a different search term.`\n            : \"There are no AI models available at this time.\"}\n        </EmptyDescription>\n      </EmptyHeader>\n    </Empty>\n  );\n}\n\ninterface ModelListProps {\n  models: AIModel[];\n  selectedModelId?: string;\n  onSelect: (model: AIModel) => void;\n  isLoading: boolean;\n}\n\nfunction ModelList({\n  models,\n  selectedModelId,\n  onSelect,\n  isLoading,\n}: ModelListProps) {\n  const { value: searchValue } = useCommandMenu();\n\n  const filteredModels = useMemo(() => {\n    if (!searchValue.trim()) return models;\n\n    const query = searchValue.toLowerCase().trim();\n    return models.filter(\n      (model) =>\n        model.name.toLowerCase().includes(query) ||\n        model.provider.toLowerCase().includes(query) ||\n        model.description?.toLowerCase().includes(query) ||\n        PROVIDER_CONFIGS[model.provider].name.toLowerCase().includes(query) ||\n        model.features?.some((feature) =>\n          FEATURE_LABELS[feature].toLowerCase().includes(query)\n        )\n    );\n  }, [models, searchValue]);\n\n  const groupedModels = useMemo(() => {\n    const groups: Record<AIProvider, AIModel[]> = {\n      openai: [],\n      anthropic: [],\n      google: [],\n      meta: [],\n      mistral: [],\n    };\n\n    filteredModels.forEach((model) => {\n      if (groups[model.provider]) {\n        groups[model.provider].push(model);\n      }\n    });\n\n    return Object.entries(groups).filter(\n      ([, providerModels]) => providerModels.length > 0\n    ) as [AIProvider, AIModel[]][];\n  }, [filteredModels]);\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center gap-2 py-8 text-muted-foreground text-sm\">\n        <span>Loading models…</span>\n      </div>\n    );\n  }\n\n  if (groupedModels.length === 0) {\n    return <EmptyState searchQuery={searchValue} />;\n  }\n\n  return (\n    <>\n      {groupedModels.map(([provider, providerModels], groupIndex) => {\n        const globalStartIndex = filteredModels.findIndex(\n          (m) => m.provider === provider\n        );\n        return (\n          <div key={provider} role=\"list\">\n            {groupIndex > 0 && <CommandMenuSeparator />}\n            <ProviderGroupHeader provider={provider} />\n            {providerModels.map((model, modelIndex) => {\n              const globalIndex = globalStartIndex + modelIndex;\n              return (\n                <ModelItem\n                  index={globalIndex}\n                  isSelected={model.id === selectedModelId}\n                  key={model.id}\n                  model={model}\n                  onSelect={() => onSelect(model)}\n                />\n              );\n            })}\n          </div>\n        );\n      })}\n    </>\n  );\n}\n\ninterface DefaultTriggerProps {\n  selectedModel: AIModel | undefined;\n  className?: string;\n  onClick?: () => void;\n}\n\nfunction DefaultTrigger({\n  selectedModel,\n  className,\n  onClick,\n}: DefaultTriggerProps) {\n  return (\n    <Button\n      aria-label={\n        selectedModel\n          ? `Selected model: ${selectedModel.name}. Click to change model.`\n          : \"Select AI model\"\n      }\n      className={cn(\"min-h-[32px] gap-2\", className)}\n      onClick={onClick}\n      type=\"button\"\n      variant=\"outline\"\n    >\n      {selectedModel ? (\n        <>\n          <div\n            aria-hidden=\"true\"\n            className=\"flex size-4 items-center justify-center text-muted-foreground\"\n          >\n            {PROVIDER_CONFIGS[selectedModel.provider].icon}\n          </div>\n          <span className=\"tabular-nums\">{selectedModel.name}</span>\n        </>\n      ) : (\n        <span>Select model</span>\n      )}\n      <ChevronDown\n        aria-hidden=\"true\"\n        className=\"size-4 text-muted-foreground\"\n      />\n    </Button>\n  );\n}\n\nexport default function AIModelSelector({\n  models = DEFAULT_MODELS,\n  selectedModelId,\n  onModelSelect,\n  trigger,\n  className,\n  isLoading = false,\n}: AIModelSelectorProps) {\n  const [open, setOpen] = useState(false);\n  const searchInputRef = useRef<HTMLInputElement>(null);\n\n  useCommandMenuShortcut(\n    useCallback(() => {\n      setOpen((prev) => !prev);\n    }, [])\n  );\n\n  useEffect(() => {\n    if (open && searchInputRef.current) {\n      setTimeout(() => {\n        searchInputRef.current?.focus();\n      }, 100);\n    }\n  }, [open]);\n\n  const selectedModel = useMemo(\n    () => models.find((m) => m.id === selectedModelId),\n    [models, selectedModelId]\n  );\n\n  const handleSelect = useCallback(\n    (model: AIModel) => {\n      onModelSelect?.(model);\n      setOpen(false);\n    },\n    [onModelSelect]\n  );\n\n  const handleOpenChange = useCallback((newOpen: boolean) => {\n    setOpen(newOpen);\n  }, []);\n\n  const handleTriggerClick = useCallback(() => {\n    setOpen(true);\n  }, []);\n\n  return (\n    <TooltipProvider>\n      <CommandMenu onOpenChange={handleOpenChange} open={open}>\n        {trigger ? (\n          <CommandMenuTrigger asChild>{trigger}</CommandMenuTrigger>\n        ) : (\n          <CommandMenuTrigger asChild>\n            <DefaultTrigger\n              className={className}\n              onClick={handleTriggerClick}\n              selectedModel={selectedModel}\n            />\n          </CommandMenuTrigger>\n        )}\n\n        <CommandMenuContent\n          aria-label=\"AI model selector\"\n          className=\"max-w-2xl\"\n          showShortcut={false}\n        >\n          <CommandMenuInput\n            aria-label=\"Search AI models\"\n            disabled={isLoading}\n            placeholder=\"Search models…\"\n            ref={searchInputRef}\n          />\n          <CommandMenuList maxHeight=\"400px\">\n            <ModelList\n              isLoading={isLoading}\n              models={models}\n              onSelect={handleSelect}\n              selectedModelId={selectedModelId}\n            />\n          </CommandMenuList>\n        </CommandMenuContent>\n      </CommandMenu>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}