{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"model-selector","type":"registry:ui","title":"Model Selector","description":"A standalone model picker — compare models in a dropdown with badges for speed, context length and what each supports.","author":"Scrim UI (https://scrimui.dev)","categories":["model-settings"],"docs":"https://scrimui.dev/components/model-selector","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/model-selector/model-selector.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type ModelOption = {\n  id: string;\n  name: string;\n  hint: string;\n  badges?: string[];\n  /**\n   * Optional leading mark, e.g. the provider's logo. A slot rather than a\n   * built-in lookup so this component stays dependency-free — pass whatever\n   * icon element you already have.\n   */\n  icon?: React.ReactNode;\n};\n\nexport type ModelSelectorProps = {\n  options: ModelOption[];\n  value?: string;\n  onSelect?: (id: string) => void;\n  placeholder?: string;\n  defaultOpen?: boolean;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction CheckIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      width=\"12\"\n      height=\"12\"\n      {...props}\n    >\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* ModelSelector                                                       */\n/* ------------------------------------------------------------------ */\n\nexport function ModelSelector({\n  options,\n  value,\n  onSelect,\n  placeholder = \"Choose a model\",\n  defaultOpen = false,\n  className = \"\",\n}: ModelSelectorProps) {\n  const [open, setOpen] = React.useState(defaultOpen);\n  const selected = options.find((o) => o.id === value) ?? options[0];\n\n  return (\n    <div className={`relative ${className}`}>\n      <button\n        type=\"button\"\n        onClick={() => setOpen((o) => !o)}\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        className=\"inline-flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-zinc-200 bg-white px-3 text-sm text-zinc-800 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800\"\n      >\n        <span className=\"flex min-w-0 items-center gap-2\">\n          <span className=\"truncate font-medium\">\n            {selected?.name ?? placeholder}\n          </span>\n          {selected?.badges?.[0] && (\n            <span className=\"shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400\">\n              {selected.badges[0]}\n            </span>\n          )}\n        </span>\n        <svg\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          width=\"14\"\n          height=\"14\"\n          className={`shrink-0 transition-transform ${open ? \"rotate-180\" : \"\"}`}\n        >\n          <path d=\"m6 9 6 6 6-6\" />\n        </svg>\n      </button>\n\n      {open && (\n        <>\n          {/* Click-away backdrop */}\n          <div className=\"fixed inset-0 z-10\" onClick={() => setOpen(false)} />\n          {/* A div, not a ul: role=\"listbox\" overrides the ul's implicit list\n              role, which makes every <li> inside it an orphaned listitem, and\n              role=\"option\" must be a *direct* child of the listbox — the <li>\n              wrapper broke that too. Buttons are not valid children of <ul>\n              anyway. */}\n          <div\n            role=\"listbox\"\n            aria-label=\"Model\"\n            className=\"absolute left-0 right-0 top-full z-20 mt-1.5 overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900\"\n          >\n            {options.map((opt) => {\n              const active = opt.id === selected?.id;\n              return (\n                <button\n                  key={opt.id}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={active}\n                  onClick={() => {\n                    onSelect?.(opt.id);\n                    setOpen(false);\n                  }}\n                  className={`flex w-full items-start gap-3 px-3.5 py-2.5 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800 ${\n                    active ? \"bg-zinc-50 dark:bg-zinc-800\" : \"\"\n                  }`}\n                >\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"flex items-center gap-1.5\">\n                      {opt.icon}\n                      <span className=\"text-sm font-medium text-zinc-900 dark:text-zinc-100\">\n                        {opt.name}\n                      </span>\n                      {active && (\n                        <span className=\"text-violet-600 dark:text-violet-400\">\n                          <CheckIcon />\n                        </span>\n                      )}\n                    </div>\n                    <p className=\"mt-0.5 text-xs text-zinc-500 dark:text-zinc-400\">\n                      {opt.hint}\n                    </p>\n                  </div>\n                  {opt.badges && opt.badges.length > 0 && (\n                    <div className=\"flex shrink-0 flex-wrap items-center gap-1 pt-0.5\">\n                      {opt.badges.map((b) => (\n                        <span\n                          key={b}\n                          className=\"rounded-full bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400\"\n                        >\n                          {b}\n                        </span>\n                      ))}\n                    </div>\n                  )}\n                </button>\n              );\n            })}\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/model-selector.tsx"}]}