{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-picker",
  "type": "registry:ui",
  "title": "Model Picker",
  "description": "A chat-style model switcher. Provider icons on a side rail filter the list to models available from that provider.",
  "author": "Aditya Ojha <https://akoder.xyz>",
  "categories": [
    "ai",
    "chat",
    "popover",
    "select",
    "interactive"
  ],
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "popover",
    "tooltip"
  ],
  "meta": {
    "docs": "https://akoder.xyz/components/model-picker",
    "source": "https://github.com/AdityaKodez/adityaojha/blob/main/components/ui/model-picker.tsx"
  },
  "files": [
    {
      "path": "components/ui/model-picker.tsx",
      "content": "\"use client\";\n\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Brain,\n  Check,\n  ChevronDown,\n  ImageIcon,\n  Search,\n  X,\n  type LucideIcon,\n} from \"lucide-react\";\nimport Image from \"next/image\";\nimport {\n  Fragment,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type ReactNode,\n  type SVGProps,\n} from \"react\";\n\nexport type IconProps = SVGProps<SVGSVGElement>;\n\n/** Real Grok mark, same paths as `ask-ai`. No file lives under `public/ai/`. */\nexport function GrokIcon(props: IconProps) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\" fill=\"currentColor\" {...props}>\n      <path d=\"M395.479 633.828 735.91 381.105c16.689-12.39 40.544-7.557 48.496 11.687 41.854 101.493 23.155 223.461-60.118 307.204-83.272 83.743-199.137 102.108-305.041 60.281l-115.691 53.866c165.934 114.059 367.431 85.852 493.345-40.861 99.875-100.439 130.807-237.345 101.884-360.806l.262.263c-41.942-181.369 10.311-253.865 117.353-402.107 2.53-3.515 5.07-7.03 7.6-10.632L883.144 141.651v-.439L395.392 633.916\" />\n      <path d=\"M325.226 695.251C206.128 580.84 226.662 403.776 328.285 301.668c75.146-75.571 198.264-106.414 305.741-61.072l115.428-53.602c-20.797-15.114-47.448-31.371-78.03-42.794-138.234-57.206-303.731-28.735-416.101 84.182C147.234 337.081 113.244 504.215 171.613 646.833c43.603 106.59-27.874 181.985-99.875 258.083C46.224 931.893 20.622 958.87 0 987.429l325.139-292.09\" />\n    </svg>\n  );\n}\n\nconst fileLogos: Record<string, { src: string; invertInDark?: boolean }> = {\n  openai: { src: \"/ai/openai.svg\", invertInDark: true },\n  anthropic: { src: \"/ai/claude.svg\" },\n  google: { src: \"/ai/gemini.svg\" },\n};\n\nexport type ModelCapability = \"reasoning\" | \"image\";\n\nexport type ThinkingEffort = \"none\" | \"low\" | \"medium\" | \"high\" | \"max\";\n\nexport type ModelPickerModel = {\n  id: string;\n  name: string;\n  description?: string;\n  /** Defaults to true. False means the row is omitted from the list. */\n  available?: boolean;\n  /** Corner chips. Reasoning means the model can think. Image means it takes or makes images. */\n  capabilities?: readonly ModelCapability[];\n  /** Selectable thinking efforts, rendered in the footer track. Omit when the model has no thinking control. */\n  thinking?: readonly ThinkingEffort[];\n  defaultThinking?: ThinkingEffort;\n};\n\nexport type ModelPickerProvider = {\n  id: string;\n  name: string;\n  icon?: ReactNode;\n  models: ModelPickerModel[];\n};\n\nexport type ModelPickerProps = {\n  providers: readonly ModelPickerProvider[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (\n    modelId: string,\n    providerId: string,\n    thinking?: ThinkingEffort,\n  ) => void;\n  thinking?: ThinkingEffort;\n  defaultThinking?: ThinkingEffort;\n  /** Close the popover as soon as a model row is picked. Off by default so the thinking track stays reachable. */\n  closeOnSelect?: boolean;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  align?: \"start\" | \"center\" | \"end\";\n  placeholder?: string;\n  className?: string;\n};\n\nconst CAPABILITY_LABEL: Record<ModelCapability, string> = {\n  reasoning: \"Reasoning\",\n  image: \"Image\",\n};\n\nconst CAPABILITY_ICON: Record<ModelCapability, LucideIcon> = {\n  reasoning: Brain,\n  image: ImageIcon,\n};\n\n/** One accent per capability so the icons read apart inside the shared pill. Literal palette colors keep the file portable. */\nconst CAPABILITY_ACCENT: Record<ModelCapability, string> = {\n  reasoning: \"text-violet-500 dark:text-violet-400\",\n  image: \"text-teal-500 dark:text-teal-400\",\n};\n\nconst THINKING_LABEL: Record<ThinkingEffort, string> = {\n  none: \"Off\",\n  low: \"Low\",\n  medium: \"Medium\",\n  high: \"High\",\n  max: \"Max\",\n};\n\nconst FULL_THINKING = [\"low\", \"medium\", \"high\", \"max\"] as const;\nconst FLASH_THINKING = [\"none\", \"low\", \"medium\"] as const;\n\n/** Thin scrollbar in both engines. Literal fallbacks keep the file portable outside this site. */\nconst THIN_SCROLLBAR =\n  \"[scrollbar-color:var(--color-border,#d4d4d8)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[var(--color-border,#d4d4d8)] [&::-webkit-scrollbar-track]:bg-transparent\";\n\n/** Hidden scrollbar for the narrow provider rail, where a visible track would eat the icons. */\nconst HIDDEN_SCROLLBAR =\n  \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\";\n\nexport const defaultModelProviders: readonly ModelPickerProvider[] = [\n  {\n    id: \"openai\",\n    name: \"OpenAI\",\n    models: [\n      {\n        id: \"gpt-5.6-sol\",\n        name: \"GPT-5.6 Sol\",\n        description: \"Flagship depth for hard problems\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"medium\",\n      },\n      {\n        id: \"gpt-5.6-terra\",\n        name: \"GPT-5.6 Terra\",\n        description: \"Balanced speed and reasoning\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"medium\",\n      },\n      {\n        id: \"gpt-5.6-luna\",\n        name: \"GPT-5.6 Luna\",\n        description: \"Fast replies for light work\",\n        capabilities: [\"image\"],\n        thinking: FLASH_THINKING,\n        defaultThinking: \"low\",\n      },\n      {\n        id: \"gpt-5.5\",\n        name: \"GPT-5.5\",\n        description: \"Previous generation, still dependable\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: [\"low\", \"medium\", \"high\"],\n        defaultThinking: \"medium\",\n      },\n    ],\n  },\n  {\n    id: \"anthropic\",\n    name: \"Anthropic\",\n    models: [\n      {\n        id: \"claude-opus-5\",\n        name: \"Claude Opus 5\",\n        description: \"Deepest reasoning, long context\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"high\",\n      },\n      {\n        id: \"claude-sonnet-5\",\n        name: \"Claude Sonnet 5\",\n        description: \"Agentic coding and tool use\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"medium\",\n      },\n      {\n        id: \"claude-haiku-4.5\",\n        name: \"Claude Haiku 4.5\",\n        description: \"Quick drafts at low cost\",\n        capabilities: [\"image\"],\n      },\n    ],\n  },\n  {\n    id: \"xai\",\n    name: \"xAI\",\n    models: [\n      {\n        id: \"grok-4.6\",\n        name: \"Grok 4.6\",\n        description: \"Flagship with live search\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"high\",\n      },\n      {\n        id: \"grok-4.20\",\n        name: \"Grok 4.20\",\n        description: \"Extended reasoning for tough prompts\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"high\",\n      },\n      {\n        id: \"grok-4.5\",\n        name: \"Grok 4.5\",\n        description: \"Previous generation, broad knowledge\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: [\"low\", \"medium\", \"high\"],\n        defaultThinking: \"medium\",\n      },\n    ],\n  },\n  {\n    id: \"google\",\n    name: \"Google\",\n    models: [\n      {\n        id: \"gemini-3.8-flash\",\n        name: \"Gemini 3.8 Flash\",\n        description: \"Fast multimodal all rounder\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"medium\",\n      },\n      {\n        id: \"gemini-3.1-pro\",\n        name: \"Gemini 3.1 Pro\",\n        description: \"Deep think for hard analysis\",\n        capabilities: [\"reasoning\", \"image\"],\n        thinking: FULL_THINKING,\n        defaultThinking: \"high\",\n      },\n      {\n        id: \"gemini-3.1-flash-image\",\n        name: \"Gemini 3.1 Flash Image\",\n        description: \"Image generation and editing\",\n        capabilities: [\"image\"],\n      },\n    ],\n  },\n];\n\nfunction isAvailable(model: ModelPickerModel) {\n  return model.available !== false;\n}\n\nfunction visibleModels(provider: ModelPickerProvider) {\n  return provider.models.filter(isAvailable);\n}\n\nfunction visibleProviders(providers: readonly ModelPickerProvider[]) {\n  return providers.filter((provider) => visibleModels(provider).length > 0);\n}\n\n/** Name, id, description, and provider name all count as a hit. */\nfunction matchesQuery(\n  model: ModelPickerModel,\n  provider: ModelPickerProvider,\n  query: string,\n) {\n  return [model.name, model.id, model.description ?? \"\", provider.name].some(\n    (field) => field.toLowerCase().includes(query),\n  );\n}\n\nfunction findModel(\n  providers: readonly ModelPickerProvider[],\n  modelId: string | undefined,\n) {\n  if (!modelId) return undefined;\n  for (const provider of providers) {\n    const model = provider.models.find((item) => item.id === modelId);\n    if (model) return { provider, model };\n  }\n  return undefined;\n}\n\nfunction ProviderGlyph({\n  provider,\n  className,\n}: {\n  provider: ModelPickerProvider;\n  className?: string;\n}) {\n  if (provider.icon) {\n    return (\n      <span className={cn(\"inline-flex size-4 items-center justify-center [&>svg]:size-full\", className)}>\n        {provider.icon}\n      </span>\n    );\n  }\n  const file = fileLogos[provider.id];\n  if (file) {\n    return (\n      <Image\n        src={file.src}\n        alt=\"\"\n        width={16}\n        height={16}\n        aria-hidden=\"true\"\n        className={cn(\"size-4\", file.invertInDark && \"dark:invert\", className)}\n      />\n    );\n  }\n  if (provider.id === \"xai\") {\n    return <GrokIcon aria-hidden=\"true\" className={cn(\"size-4\", className)} />;\n  }\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"inline-flex size-4 items-center justify-center text-[11px] font-medium leading-none\",\n        className,\n      )}\n    >\n      {provider.name.charAt(0)}\n    </span>\n  );\n}\n\nfunction CapabilityChips({\n  capabilities,\n}: {\n  capabilities?: readonly ModelCapability[];\n}) {\n  if (!capabilities?.length) return null;\n  return (\n    <span className=\"inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-1 ring-1 ring-inset ring-border/70\">\n      {capabilities.map((capability, index) => {\n        const Icon = CAPABILITY_ICON[capability];\n        const label = CAPABILITY_LABEL[capability];\n        return (\n          <Fragment key={capability}>\n            {index > 0 ? (\n              <span aria-hidden=\"true\" className=\"h-3 w-px shrink-0 bg-border/70\" />\n            ) : null}\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <span\n                  aria-label={label}\n                  className={cn(\n                    \"inline-flex size-4 items-center justify-center opacity-90 transition-opacity duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:opacity-100 motion-reduce:transition-none\",\n                    CAPABILITY_ACCENT[capability],\n                  )}\n                >\n                  <Icon aria-hidden=\"true\" className=\"size-3\" strokeWidth={2} />\n                </span>\n              </TooltipTrigger>\n              <TooltipContent side=\"top\" className=\"text-xs\">\n                {label}\n              </TooltipContent>\n            </Tooltip>\n          </Fragment>\n        );\n      })}\n    </span>\n  );\n}\n\n/** Stepped bars that fill up to the current effort, each in its own accent. Decorative. */\nfunction EffortMeter({\n  levels,\n  filled,\n  className,\n}: {\n  levels: readonly ThinkingEffort[];\n  filled: number;\n  className?: string;\n}) {\n  return (\n    <span aria-hidden=\"true\" className={cn(\"flex items-end gap-0.5\", className)}>\n      {levels.map((effort, index) => (\n        <span\n          key={effort}\n          style={{ height: `${5 + index * 2.5}px` }}\n          className={cn(\n            \"w-0.75 rounded-full transition-colors duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\",\n            index < filled ? \"bg-foreground\" : \"bg-border\",\n          )}\n        />\n      ))}\n    </span>\n  );\n}\n\n/** Segmented track with a sliding indicator. One control for the whole popover. */\nfunction ThinkingTrack({\n  levels,\n  value,\n  onChange,\n}: {\n  levels: readonly ThinkingEffort[];\n  value?: ThinkingEffort;\n  onChange: (effort: ThinkingEffort) => void;\n}) {\n  const refs = useRef<Array<HTMLButtonElement | null>>([]);\n  const current = value && levels.includes(value) ? value : levels[0];\n  const activeIndex = Math.max(0, levels.indexOf(current));\n\n  function move(index: number) {\n    const next = (index + levels.length) % levels.length;\n    onChange(levels[next]);\n    refs.current[next]?.focus();\n  }\n\n  function onKeyDown(event: KeyboardEvent<HTMLButtonElement>) {\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n      event.preventDefault();\n      move(activeIndex + 1);\n    } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      move(activeIndex - 1);\n    } else if (event.key === \"Home\") {\n      event.preventDefault();\n      move(0);\n    } else if (event.key === \"End\") {\n      event.preventDefault();\n      move(levels.length - 1);\n    }\n  }\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label=\"Thinking effort\"\n      className=\"relative flex min-w-0 flex-1 items-center rounded-full bg-muted/60 p-1 ring-1 ring-inset ring-border/60\"\n    >\n      <span\n        aria-hidden=\"true\"\n        style={{\n          width: `calc((100% - 0.5rem) / ${levels.length})`,\n          transform: `translateX(${activeIndex * 100}%)`,\n        }}\n        className=\"pointer-events-none absolute inset-y-1 left-1 rounded-full bg-popover ring-1 ring-inset ring-border/70 transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\"\n      />\n      {levels.map((effort, index) => {\n        const active = index === activeIndex;\n        return (\n          <button\n            key={effort}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            tabIndex={active ? 0 : -1}\n            ref={(node) => {\n              refs.current[index] = node;\n            }}\n            onClick={() => onChange(effort)}\n            onKeyDown={onKeyDown}\n            className={cn(\n              \"relative z-10 min-w-0 flex-1 cursor-pointer touch-manipulation rounded-full px-1.5 py-1.5 text-center text-[11px] font-medium leading-none text-muted-foreground transition-colors duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none motion-reduce:transition-none\",\n              active && \"text-foreground\",\n            )}\n          >\n            {THINKING_LABEL[effort]}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function ModelPicker({\n  providers,\n  value,\n  defaultValue,\n  onValueChange,\n  thinking,\n  defaultThinking,\n  closeOnSelect = false,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  side = \"top\",\n  align = \"start\",\n  placeholder = \"Select a model\",\n  className,\n}: ModelPickerProps) {\n  const listId = useId();\n  const rails = useMemo(() => visibleProviders(providers), [providers]);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const selectedId = value ?? internalValue;\n  const isOpen = open ?? internalOpen;\n  const selected = findModel(providers, selectedId);\n  const [internalThinking, setInternalThinking] = useState<ThinkingEffort | undefined>(\n    defaultThinking ?? selected?.model.defaultThinking,\n  );\n  const selectedThinking = thinking ?? internalThinking;\n\n  const selectedProviderId = selected?.provider.id;\n\n  const [activeProviderId, setActiveProviderId] = useState(\n    () => findModel(providers, selectedId)?.provider.id ?? rails[0]?.id ?? \"\",\n  );\n\n  const [query, setQuery] = useState(\"\");\n  const search = query.trim().toLowerCase();\n  const searching = search.length > 0;\n\n  const activeProvider =\n    rails.find((provider) => provider.id === activeProviderId) ?? rails[0];\n  /** Search spans every provider. Without a query the list stays scoped to the active rail. */\n  const rows = useMemo(() => {\n    if (searching) {\n      return rails.flatMap((provider) =>\n        visibleModels(provider)\n          .filter((model) => matchesQuery(model, provider, search))\n          .map((model) => ({ provider, model })),\n      );\n    }\n    if (!activeProvider) return [];\n    return visibleModels(activeProvider).map((model) => ({\n      provider: activeProvider,\n      model,\n    }));\n  }, [activeProvider, rails, search, searching]);\n\n  const activeModelIndex = Math.max(\n    0,\n    rows.findIndex((row) => row.model.id === selectedId),\n  );\n\n  const providerRefs = useRef<Array<HTMLButtonElement | null>>([]);\n  const modelRefs = useRef<Array<HTMLElement | null>>([]);\n  const searchRef = useRef<HTMLInputElement | null>(null);\n  const railRef = useRef<HTMLDivElement | null>(null);\n\n  /**\n   * Centre the selected provider in the rail when the panel opens. Past roughly a\n   * dozen providers the rail scrolls, and the selected one can sit below the fold,\n   * so the panel would otherwise open with no visible active tab.\n   *\n   * Keyed on the selected provider rather than the active tab so clicking through\n   * the rail never yanks the scroll position out from under the pointer. The\n   * double frame is needed because the popover is portalled: on the first commit\n   * the rail has no height yet and any scroll would clamp to zero.\n   */\n  useEffect(() => {\n    if (!isOpen || searching) return;\n    const index = rails.findIndex(\n      (provider) => provider.id === selectedProviderId,\n    );\n    if (index < 0) return;\n    let inner = 0;\n    const outer = requestAnimationFrame(() => {\n      inner = requestAnimationFrame(() => {\n        const button = providerRefs.current[index];\n        const rail = railRef.current;\n        if (!button || !rail) return;\n        if (rail.scrollHeight <= rail.clientHeight) return;\n        const offset =\n          button.offsetTop - (rail.clientHeight - button.offsetHeight) / 2;\n        rail.scrollTop = Math.max(0, offset);\n      });\n    });\n    return () => {\n      cancelAnimationFrame(outer);\n      cancelAnimationFrame(inner);\n    };\n  }, [isOpen, rails, searching, selectedProviderId]);\n\n  const changeOpen = useCallback(\n    (next: boolean) => {\n      setInternalOpen(next);\n      onOpenChange?.(next);\n      setQuery(\"\");\n      if (next) {\n        const owner = findModel(providers, selectedId)?.provider.id;\n        if (owner) setActiveProviderId(owner);\n      }\n    },\n    [onOpenChange, providers, selectedId],\n  );\n\n  const selectModel = useCallback(\n    (modelId: string, providerId: string) => {\n      const found = findModel(providers, modelId)?.model;\n      const effort = found?.defaultThinking ?? found?.thinking?.[0];\n      setInternalValue(modelId);\n      setInternalThinking(effort);\n      onValueChange?.(modelId, providerId, effort);\n      if (closeOnSelect) changeOpen(false);\n    },\n    [changeOpen, closeOnSelect, onValueChange, providers],\n  );\n\n  const selectThinking = useCallback(\n    (effort: ThinkingEffort) => {\n      setInternalThinking(effort);\n      if (selected) {\n        onValueChange?.(selected.model.id, selected.provider.id, effort);\n      }\n    },\n    [onValueChange, selected],\n  );\n\n  function focusProvider(index: number) {\n    const next = (index + rails.length) % rails.length;\n    providerRefs.current[next]?.focus();\n  }\n\n  function focusModel(index: number) {\n    if (rows.length === 0) return;\n    const next = (index + rows.length) % rows.length;\n    modelRefs.current[next]?.focus();\n  }\n\n  function onSearchKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      focusModel(0);\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      const first = rows[0];\n      if (first) selectModel(first.model.id, first.provider.id);\n    } else if (event.key === \"Escape\" && query) {\n      event.preventDefault();\n      event.stopPropagation();\n      setQuery(\"\");\n    }\n  }\n\n  function onRailKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      focusProvider(index + 1);\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      focusProvider(index - 1);\n    } else if (event.key === \"Home\") {\n      event.preventDefault();\n      focusProvider(0);\n    } else if (event.key === \"End\") {\n      event.preventDefault();\n      focusProvider(rails.length - 1);\n    } else if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      focusModel(0);\n    }\n  }\n\n  function onListKeyDown(event: KeyboardEvent<HTMLElement>, index: number) {\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      focusModel(index + 1);\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      focusModel(index - 1);\n    } else if (event.key === \"Home\") {\n      event.preventDefault();\n      focusModel(0);\n    } else if (event.key === \"End\") {\n      event.preventDefault();\n      focusModel(rows.length - 1);\n    } else if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      const railIndex = rails.findIndex((provider) => provider.id === activeProvider?.id);\n      providerRefs.current[railIndex]?.focus();\n    }\n  }\n\n  const thinkingLevels = selected?.model.thinking;\n  const effortIndex = thinkingLevels && selectedThinking\n    ? thinkingLevels.indexOf(selectedThinking)\n    : -1;\n  const filledSteps =\n    !thinkingLevels || selectedThinking === \"none\" || effortIndex < 0\n      ? 0\n      : effortIndex + 1;\n  const thinkingLabel =\n    thinkingLevels && selectedThinking && selectedThinking !== \"none\"\n      ? THINKING_LABEL[selectedThinking]\n      : null;\n  const triggerLabel = selected?.model.name ?? placeholder;\n\n  return (\n    <TooltipProvider delayDuration={250}>\n      <Popover open={isOpen} onOpenChange={changeOpen} >\n        <PopoverTrigger\n          type=\"button\"\n          aria-label={triggerLabel}\n          aria-haspopup=\"listbox\"\n          className={cn(\n            \"inline-flex h-11 max-w-full cursor-pointer touch-manipulation items-center gap-2 rounded-full bg-background px-3 text-sm font-medium tracking-tight text-foreground ring-1 ring-inset ring-border/80 transition-[background-color,box-shadow,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:bg-muted/70 active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-muted/70 motion-reduce:transition-none motion-reduce:active:scale-100 sm:px-4\",\n            className,\n          )}\n        >\n          {selected ? (\n            <span className=\"inline-flex size-6 shrink-0 items-center justify-center text-foreground sm:size-7\">\n              <ProviderGlyph provider={selected.provider} />\n            </span>\n          ) : null}\n          <span className=\"min-w-0 truncate\" translate=\"no\">\n            {triggerLabel}\n          </span>\n          {thinkingLabel ? (\n            <span className=\"hidden shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline-flex\">\n              <EffortMeter levels={thinkingLevels ?? []} filled={filledSteps} />\n              \n            </span>\n          ) : null}\n          <ChevronDown\n            aria-hidden=\"true\"\n            className=\"size-3.5 shrink-0 text-muted-foreground transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none data-[open=true]:rotate-180\"\n            data-open={isOpen}\n          />\n        </PopoverTrigger>\n        <PopoverContent\n          side={side}\n          align={align}\n          sideOffset={10}\n          // Pinned to `side`. Collision flipping is what made the panel open above the\n          // trigger at one scroll position and below it at the next.\n          avoidCollisions={false}\n          className=\"max-h-[calc(100dvh-2rem)] w-[min(26rem,calc(100vw-1.5rem))] gap-0 overflow-hidden overscroll-contain rounded-xl p-0\"\n          onOpenAutoFocus={(event) => {\n            event.preventDefault();\n            requestAnimationFrame(() => {\n              searchRef.current?.focus();\n            });\n          }}\n        >\n          {rails.length === 0 || !activeProvider ? (\n            <p className=\"px-4 py-6 text-sm text-muted-foreground\">No models available</p>\n          ) : (\n            <div className=\"flex min-w-0 flex-col\">\n             \n              <div className=\"flex min-h-52 bg-muted\">\n                {/*\n                  The rail scroller is taken out of flow so a long provider list cannot\n                  inflate the row. Left in flow it grows to its natural height and drags\n                  the panel off screen instead of scrolling, because `overflow-y-auto`\n                  needs a bounded height and a stretched flex item never gets one here.\n                  Out of flow, the row height comes from the model column and the rail\n                  scrolls inside it at any provider count.\n                */}\n                <div className=\"relative w-12 shrink-0 bg-muted sm:w-14\">\n                <div\n                  role=\"tablist\"\n                  aria-label=\"Providers\"\n                  aria-orientation=\"vertical\"\n                  ref={railRef}\n                  className={cn(\n                    \"absolute inset-0 flex flex-col gap-1.5 overflow-y-auto overscroll-contain p-1.5\",\n                    HIDDEN_SCROLLBAR,\n                  )}\n                >\n                  {rails.map((provider, index) => {\n                    const selectedRail = provider.id === activeProvider.id;\n                    return (\n                      <Tooltip key={provider.id}>\n                        <TooltipTrigger asChild>\n                          <button\n                            type=\"button\"\n                            role=\"tab\"\n                            id={`${listId}-tab-${provider.id}`}\n                            aria-label={provider.name}\n                            aria-selected={selectedRail}\n                            aria-controls={listId}\n                            tabIndex={selectedRail ? 0 : -1}\n                            ref={(node) => {\n                              providerRefs.current[index] = node;\n                            }}\n                            onClick={() => {\n                              setQuery(\"\");\n                              setActiveProviderId(provider.id);\n                            }}\n                            onKeyDown={(event) => onRailKeyDown(event, index)}\n                            className={cn(\n                              \"inline-flex size-9 shrink-0 cursor-pointer touch-manipulation items-center justify-center rounded-xl text-muted-foreground transition-[background-color,color,box-shadow] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:bg-popover/70 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none sm:size-11\",\n                              selectedRail &&\n                                !searching &&\n                                \"bg-popover text-foreground ring-1 ring-inset ring-border/60\",\n                            )}\n                          >\n                            <ProviderGlyph provider={provider} />\n                          </button>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"right\" className=\"text-xs\" translate=\"no\">\n                          {provider.name}\n                        </TooltipContent>\n                      </Tooltip>\n                    );\n                  })}\n                </div>\n                </div>\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n  <div className=\"flex shrink-0 items-center gap-2 bg-muted px-3 py-2\">\n                <Search\n                  aria-hidden=\"true\"\n                  className=\"size-3.5 shrink-0 text-muted-foreground\"\n                  strokeWidth={2}\n                />\n                <input\n                  ref={searchRef}\n                  type=\"text\"\n                  value={query}\n                  onChange={(event) => setQuery(event.target.value)}\n                  onKeyDown={onSearchKeyDown}\n                  placeholder=\"Search models\"\n                  aria-label=\"Search models\"\n                  aria-controls={listId}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  className=\"h-6 w-full min-w-0 bg-transparent text-sm tracking-tight text-foreground placeholder:text-muted-foreground focus-visible:outline-none\"\n                />\n                {query ? (\n                  <button\n                    type=\"button\"\n                    aria-label=\"Clear search\"\n                    onClick={() => {\n                      setQuery(\"\");\n                      searchRef.current?.focus();\n                    }}\n                    className=\"inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:bg-popover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none motion-reduce:transition-none\"\n                  >\n                    <X aria-hidden=\"true\" className=\"size-3\" strokeWidth={2} />\n                  </button>\n                ) : null}\n              </div>\n        \n                <div\n                  key={searching ? \"search\" : activeProvider.id}\n                  className=\"flex min-w-0 flex-1 flex-col rounded-tl-lg bg-popover p-2 motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300\"\n                >\n                  <p\n                    className=\"px-2.5 pb-1.5 pt-1 font-mono text-[11px] text-muted-foreground\"\n                    translate=\"no\"\n                  >\n                    {searching\n                      ? `${rows.length} ${rows.length === 1 ? \"result\" : \"results\"}`\n                      : activeProvider.name}\n                  </p>\n                  <div\n                    role=\"listbox\"\n                    id={listId}\n                    aria-label={searching ? \"Search results\" : `${activeProvider.name} models`}\n                    className={cn(\n                      \"-mr-1 flex max-h-[min(16rem,42dvh)] min-w-0 flex-col gap-0.5 overflow-y-auto overscroll-contain pr-1\",\n                      THIN_SCROLLBAR,\n                    )}\n                  >\n                    {rows.length === 0 ? (\n                      <p\n                        role=\"presentation\"\n                        className=\"px-2.5 py-6 text-center text-[11px] text-muted-foreground\"\n                      >\n                        {`No models match \"${query.trim()}\"`}\n                      </p>\n                    ) : null}\n                    {rows.map(({ provider, model }, index) => {\n                      const isSelected = model.id === selectedId;\n                      return (\n                        <div\n                          key={model.id}\n                          role=\"option\"\n                          aria-selected={isSelected}\n                          tabIndex={index === activeModelIndex ? 0 : -1}\n                          ref={(node) => {\n                            modelRefs.current[index] = node;\n                          }}\n                          onClick={() => selectModel(model.id, provider.id)}\n                          onKeyDown={(event) => {\n                            if (event.key === \"Enter\" || event.key === \" \") {\n                              event.preventDefault();\n                              selectModel(model.id, provider.id);\n                              return;\n                            }\n                            onListKeyDown(event, index);\n                          }}\n                          className={cn(\n                            \"flex min-h-11 w-full min-w-0 cursor-pointer touch-manipulation items-center justify-between gap-2 rounded-xl px-2.5 py-2 text-left transition-[background-color] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n                            isSelected && \"bg-muted\",\n                          )}\n                        >\n                          {searching ? (\n                            <span className=\"inline-flex size-6 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ring-border/60\">\n                              <ProviderGlyph provider={provider} />\n                            </span>\n                          ) : null}\n                          <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                            <span className=\"flex w-full min-w-0 items-center gap-1.5\">\n                              <span\n                                className=\"min-w-0 truncate text-sm font-medium tracking-tight\"\n                                translate=\"no\"\n                              >\n                                {model.name}\n                              </span>\n                              {isSelected ? (\n                                <Check aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-foreground\" />\n                              ) : null}\n                            </span>\n                            {model.description ? (\n                              <span className=\"w-full truncate text-[11px] leading-snug text-muted-foreground\">\n                                {model.description}\n                              </span>\n                            ) : null}\n                          </span>\n                          <CapabilityChips capabilities={model.capabilities} />\n                        </div>\n                      );\n                    })}\n                  </div>\n                </div>\n                <div className=\"flex flex-col gap-2 bg-popover p-2 sm:flex-row sm:items-center sm:gap-3 sm:px-3 sm:py-2\">\n                <span className=\"flex shrink-0 items-center gap-2 px-1 sm:px-0\">\n                  <EffortMeter\n                    levels={thinkingLevels ?? FULL_THINKING}\n                    filled={filledSteps}\n                  />\n                  <span className=\"font-mono text-[11px] text-muted-foreground\">\n                    Thinking\n                  </span>\n                </span>\n                {thinkingLevels?.length ? (\n                  <ThinkingTrack\n                    levels={thinkingLevels}\n                    value={selectedThinking}\n                    onChange={selectThinking}\n                  />\n                ) : (\n                  <span className=\"px-1 text-[11px] text-muted-foreground sm:px-0\">\n                    {selected\n                      ? \"Not available for this model\"\n                      : \"Select a model to set the effort\"}\n                  </span>\n                )}\n              </div>\n              </div>\n</div>\n              \n            </div>\n          )}\n        </PopoverContent>\n      </Popover>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "public/ai/openai.svg",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"260\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 256 260\"><path d=\"M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z\"/></svg>",
      "type": "registry:file"
    },
    {
      "path": "public/ai/claude.svg",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"257\" preserveAspectRatio=\"xMidYMid\" viewBox=\"0 0 256 257\"><path fill=\"#D97757\" d=\"m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z\"/></svg>",
      "type": "registry:file"
    },
    {
      "path": "public/ai/gemini.svg",
      "content": "<svg viewBox=\"0 0 296 298\" xmlns=\"http://www.w3.org/2000/svg\" width=\"296\" height=\"298\" fill=\"none\"><mask id=\"a\" width=\"296\" height=\"298\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"#3186FF\" d=\"M141.201 4.886c2.282-6.17 11.042-6.071 13.184.148l5.985 17.37a184.004 184.004 0 0 0 111.257 113.049l19.304 6.997c6.143 2.227 6.156 10.91.02 13.155l-19.35 7.082a184.001 184.001 0 0 0-109.495 109.385l-7.573 20.629c-2.241 6.105-10.869 6.121-13.133.025l-7.908-21.296a184 184 0 0 0-109.02-108.658l-19.698-7.239c-6.102-2.243-6.118-10.867-.025-13.132l20.083-7.467A183.998 183.998 0 0 0 133.291 26.28l7.91-21.394Z\"/></mask><g mask=\"url(#a)\"><g filter=\"url(#b)\"><ellipse cx=\"163\" cy=\"149\" fill=\"#3689FF\" rx=\"196\" ry=\"159\"/></g><g filter=\"url(#c)\"><ellipse cx=\"33.5\" cy=\"142.5\" fill=\"#F6C013\" rx=\"68.5\" ry=\"72.5\"/></g><g filter=\"url(#d)\"><ellipse cx=\"19.5\" cy=\"148.5\" fill=\"#F6C013\" rx=\"68.5\" ry=\"72.5\"/></g><g filter=\"url(#e)\"><path fill=\"#FA4340\" d=\"M194 10.5C172 82.5 65.5 134.333 22.5 135L144-66l50 76.5Z\"/></g><g filter=\"url(#f)\"><path fill=\"#FA4340\" d=\"M190.5-12.5C168.5 59.5 62 111.333 19 112L140.5-89l50 76.5Z\"/></g><g filter=\"url(#g)\"><path fill=\"#14BB69\" d=\"M194.5 279.5C172.5 207.5 66 155.667 23 155l121.5 201 50-76.5Z\"/></g><g filter=\"url(#h)\"><path fill=\"#14BB69\" d=\"M196.5 320.5C174.5 248.5 68 196.667 25 196l121.5 201 50-76.5Z\"/></g></g><defs><filter id=\"b\" width=\"464\" height=\"390\" x=\"-69\" y=\"-46\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"18\"/></filter><filter id=\"c\" width=\"265\" height=\"273\" x=\"-99\" y=\"6\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter><filter id=\"d\" width=\"265\" height=\"273\" x=\"-113\" y=\"12\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter><filter id=\"e\" width=\"299.5\" height=\"329\" x=\"-41.5\" y=\"-130\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter><filter id=\"f\" width=\"299.5\" height=\"329\" x=\"-45\" y=\"-153\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter><filter id=\"g\" width=\"299.5\" height=\"329\" x=\"-41\" y=\"91\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter><filter id=\"h\" width=\"299.5\" height=\"329\" x=\"-39\" y=\"132\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_69_17998\" stdDeviation=\"32\"/></filter></defs></svg>",
      "type": "registry:file"
    }
  ],
  "docs": "## Installation\n\n```bash\nnpx shadcn@latest add https://akoder.xyz/r/model-picker.json\n```\n\n## Usage\n\n```tsx\nimport { useState } from \"react\";\nimport { ModelPicker, defaultModelProviders } from \"@/components/ui/model-picker\";\n\nexport function ChatComposer() {\n  const [model, setModel] = useState(\"grok-4.6\");\n  return (\n    <ModelPicker\n      providers={defaultModelProviders}\n      value={model}\n      onValueChange={setModel}\n    />\n  );\n}\n```"
}