{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"prompt-input-model-selector","type":"registry:ui","title":"Prompt Input + Model Selector","description":"A chat input with the model picker built in — switch models inline, with badges for what each one can do.","author":"Scrim UI (https://scrimui.dev)","categories":["prompt-input"],"docs":"https://scrimui.dev/components/prompt-input-model-selector","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/prompt-input-model-selector/prompt-input-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  description?: 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 PromptInputModelSelectorProps = {\n  models: ModelOption[];\n  defaultModel?: string;\n  placeholder?: string;\n  disabled?: boolean;\n  onSubmit?: (value: string, modelId: string) => void;\n  onChange?: (modelId: string) => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction ArrowUpIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"16\" height=\"16\" {...props}>\n      <path d=\"M12 19V5M5 12l7-7 7 7\" />\n    </svg>\n  );\n}\n\nfunction ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>\n  );\n}\n\nfunction CheckIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\n\nfunction SparkIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"M12 3v2M12 19v2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M3 12h2M19 12h2M6 7l-1-1M18 17l1 1M12 8l1.5 3.5L17 13l-3.5 1.5L12 18l-1.5-3.5L7 13l3.5-1.5Z\" />\n    </svg>\n  );\n}\n\nfunction BoltIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* PromptInputModelSelector                                            */\n/* ------------------------------------------------------------------ */\n\nexport function PromptInputModelSelector({\n  models,\n  defaultModel,\n  placeholder = \"Ask anything…\",\n  disabled = false,\n  onSubmit,\n  onChange,\n  className = \"\",\n}: PromptInputModelSelectorProps) {\n  const [value, setValue] = React.useState(\"\");\n  const [modelId, setModelId] = React.useState(defaultModel ?? models[0]?.id ?? \"\");\n  const [open, setOpen] = React.useState(false);\n  const menuRef = React.useRef<HTMLDivElement>(null);\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n\n  const active = models.find((m) => m.id === modelId);\n  const canSubmit = value.trim().length > 0 && !disabled;\n\n  React.useEffect(() => {\n    const el = textareaRef.current;\n    if (!el) return;\n    el.style.height = \"0px\";\n    el.style.height = `${Math.min(el.scrollHeight, 160)}px`;\n  }, [value]);\n\n  React.useEffect(() => {\n    if (!open) return;\n    function onClick(e: MouseEvent) {\n      if (menuRef.current && !menuRef.current.contains(e.target as Node)) setOpen(false);\n    }\n    function onKey(e: KeyboardEvent) {\n      if (e.key === \"Escape\") setOpen(false);\n    }\n    document.addEventListener(\"mousedown\", onClick);\n    document.addEventListener(\"keydown\", onKey);\n    return () => {\n      document.removeEventListener(\"mousedown\", onClick);\n      document.removeEventListener(\"keydown\", onKey);\n    };\n  }, [open]);\n\n  function selectModel(id: string) {\n    if (id === modelId) return;\n    setModelId(id);\n    onChange?.(id);\n    setOpen(false);\n    textareaRef.current?.focus();\n  }\n\n  function submit() {\n    if (!canSubmit) return;\n    onSubmit?.(value.trim(), modelId);\n    setValue(\"\");\n  }\n\n  return (\n    <div className={`w-full ${className}`}>\n      {/* axe reports the dimmed model hint here as a 2.34:1 contrast failure.\n          Leave it. WCAG 2.1 SC 1.4.3 exempts \"text that is part of an inactive\n          user interface component\" from the contrast minimum, and axe cannot\n          see that this subtree is disabled. Darkening it would make the\n          disabled state read as enabled — a real regression in service of a\n          misapplied rule. */}\n      <div\n        className={`rounded-2xl border bg-white shadow-sm transition-colors dark:bg-zinc-900 ${\n          disabled\n            ? \"border-zinc-200 opacity-60 dark:border-zinc-800\"\n            : \"border-zinc-200 focus-within:border-zinc-400 dark:border-zinc-800 dark:focus-within:border-zinc-600\"\n        }`}\n      >\n        {/* Model bar */}\n        <div className=\"flex items-center gap-2 border-b border-zinc-100 px-2.5 pt-2 pb-2 dark:border-zinc-800\">\n          <div className=\"relative\" ref={menuRef}>\n            <button\n              type=\"button\"\n              onClick={() => setOpen((v) => !v)}\n              disabled={disabled}\n              aria-haspopup=\"listbox\"\n              aria-expanded={open}\n              className=\"inline-flex h-7 items-center gap-1.5 rounded-lg bg-zinc-100 px-2.5 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700\"\n            >\n              <SparkIcon className=\"text-zinc-400\" />\n              {active?.icon}\n              {active?.name ?? \"Select model\"}\n              <ChevronDownIcon className={open ? \"rotate-180 transition-transform text-zinc-400\" : \"text-zinc-400 transition-transform\"} />\n            </button>\n\n            {open && (\n              <div\n                role=\"listbox\"\n                className=\"absolute left-0 top-full z-20 mt-1.5 w-72 overflow-hidden rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-800\"\n              >\n                {models.map((m) => (\n                  <button\n                    key={m.id}\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={m.id === modelId}\n                    onClick={() => selectModel(m.id)}\n                    className={`flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2.5 text-left transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-700 ${\n                      m.id === modelId ? \"bg-zinc-50 dark:bg-zinc-700/60\" : \"\"\n                    }`}\n                  >\n                    <span className=\"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center text-zinc-400\">\n                      {m.id === modelId ? <CheckIcon className=\"text-emerald-500\" /> : <BoltIcon />}\n                    </span>\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"flex items-center gap-1.5 text-sm font-medium text-zinc-800 dark:text-zinc-100\">\n                        {m.icon}\n                        {m.name}\n                        {m.badges?.map((b) => (\n                          <span\n                            key={b}\n                            className=\"rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-medium text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300\"\n                          >\n                            {b}\n                          </span>\n                        ))}\n                      </span>\n                      {m.description && (\n                        <span className=\"mt-0.5 block text-xs leading-4 text-zinc-500 dark:text-zinc-400\">\n                          {m.description}\n                        </span>\n                      )}\n                    </span>\n                  </button>\n                ))}\n              </div>\n            )}\n          </div>\n\n          {active?.description && (\n            <span className=\"hidden truncate text-xs text-zinc-500 dark:text-zinc-400 sm:block\">{active.description}</span>\n          )}\n        </div>\n\n        {/* Input */}\n        <textarea\n          ref={textareaRef}\n          rows={1}\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && !e.shiftKey) {\n              e.preventDefault();\n              submit();\n            }\n          }}\n          placeholder={placeholder}\n          disabled={disabled}\n          aria-label=\"Prompt\"\n          className=\"block w-full resize-none bg-transparent px-4 pt-3 pb-1.5 text-[15px] leading-6 text-zinc-900 outline-none placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500\"\n        />\n\n        <div className=\"flex items-center justify-end px-2.5 pb-2.5\">\n          <button\n            type=\"button\"\n            onClick={submit}\n            disabled={!canSubmit}\n            aria-label=\"Send message\"\n            className=\"inline-flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-900 text-white transition-opacity hover:opacity-80 disabled:opacity-30 dark:bg-zinc-100 dark:text-zinc-900\"\n          >\n            <ArrowUpIcon />\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/prompt-input-model-selector.tsx"}]}