{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "composer",
  "title": "Composer",
  "description": "A prompt box with attachment chips, a model picker and a live context meter.",
  "registryDependencies": [
    "https://spotcheckui.com/r/spotcheck-base.json",
    "https://spotcheckui.com/r/chip.json",
    "https://spotcheckui.com/r/select.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/components/spotcheck/composer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Chip } from \"@/registry/spotcheck/components/spotcheck/chip\"\nimport { Select, type SelectOption } from \"@/registry/spotcheck/components/spotcheck/select\"\n\nconst CHARS_PER_TOKEN = 4\n\nconst subscribeNever = () => () => {}\n\nfunction useSendHint() {\n  return React.useSyncExternalStore(\n    subscribeNever,\n    () => (/Mac|iPhone|iPad/.test(navigator.platform) ? \"⌘↵\" : \"Ctrl ↵\"),\n    () => \"Ctrl ↵\",\n  )\n}\n\ntype ComposerProps = {\n  value: string\n  onValueChange: (value: string) => void\n  /** Runs on the Send button and on Cmd or Ctrl plus Enter. */\n  onSubmit: () => void\n  /** File names shown as chips. */\n  attachments?: string[]\n  /** Removes a file. Leave it out to make chips read only. */\n  onRemove?: (name: string) => void\n  /** Shows an Attach button. */\n  onAttach?: () => void\n  models?: SelectOption[]\n  model?: string\n  onModelChange?: (model: string) => void\n  /** The model's context window, in tokens. */\n  contextLimit?: number\n  /** Tokens already used by attachments and history. */\n  usedTokens?: number\n  /** Stops sending, such as while a reply streams. */\n  disabled?: boolean\n  placeholder?: string\n  className?: string\n}\n\nexport function Composer({\n  value,\n  onValueChange,\n  onSubmit,\n  attachments = [],\n  onRemove,\n  onAttach,\n  models = [],\n  model,\n  onModelChange,\n  contextLimit,\n  usedTokens = 0,\n  disabled = false,\n  placeholder = \"Ask something\",\n  className,\n}: ComposerProps) {\n  const hint = useSendHint()\n  const canSend = value.trim().length > 0 && !disabled\n  const tokens = usedTokens + Math.ceil(value.length / CHARS_PER_TOKEN)\n  const share = contextLimit ? Math.min(1, tokens / contextLimit) : 0\n\n  return (\n    <div className={cn(\"border-ink bg-raise border-(length:--rule) font-(family-name:--f-body)\", className)}>\n      <label className=\"block px-3.5 pt-2.5\">\n        <span className=\"spotcheck-label block\">Prompt</span>\n        <textarea\n          value={value}\n          placeholder={placeholder}\n          onChange={(event) => onValueChange(event.target.value)}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\" && (event.metaKey || event.ctrlKey) && canSend) {\n              event.preventDefault()\n              onSubmit()\n            }\n          }}\n          className=\"placeholder:text-mute min-h-[84px] w-full resize-none bg-transparent py-2 text-base outline-none\"\n        />\n      </label>\n      {(attachments.length > 0 || onAttach) && (\n        <div className=\"flex flex-wrap gap-1.5 px-3.5 pb-2.5\">\n          {attachments.map((name) => (\n            <Chip key={name} removeLabel={`Remove ${name}`} onRemove={onRemove && (() => onRemove(name))}>\n              {name}\n            </Chip>\n          ))}\n          {onAttach && (\n            <button\n              type=\"button\"\n              onClick={onAttach}\n              className=\"spotcheck-focus border-ink cursor-pointer border-[1.5px] px-2 py-0.5 font-(family-name:--f-mono) text-[11.5px]\"\n            >\n              + Attach\n            </button>\n          )}\n        </div>\n      )}\n      <div className=\"border-ink flex flex-wrap items-stretch border-t-(length:--rule) font-(family-name:--f-mono) text-xs\">\n        {models.length > 0 && (\n          <Select\n            label=\"Model\"\n            options={models}\n            value={model}\n            onValueChange={onModelChange}\n            className=\"min-w-28 border-0 border-r-(length:--rule)\"\n          />\n        )}\n        {contextLimit !== undefined && (\n          <div className=\"flex min-w-[140px] flex-1 items-center gap-2.5 px-3\">\n            <span className=\"tabular-nums\">{Math.round(share * 100)}%</span>\n            <div\n              role=\"progressbar\"\n              aria-label=\"Context used\"\n              aria-valuemin={0}\n              aria-valuemax={contextLimit}\n              aria-valuenow={Math.min(tokens, contextLimit)}\n              className=\"bg-wash h-1.5 flex-1\"\n            >\n              <div className=\"bg-ink h-full\" style={{ width: `${share * 100}%` }} />\n            </div>\n            <span className=\"tabular-nums\">of {Math.round(contextLimit / 1000)}k</span>\n          </div>\n        )}\n        <button\n          type=\"button\"\n          disabled={!canSend}\n          onClick={onSubmit}\n          className=\"bg-accent-ink text-on-accent disabled:bg-wash disabled:text-mute spotcheck-focus ml-auto cursor-pointer px-4 py-2 font-semibold tracking-[.08em] uppercase disabled:cursor-not-allowed\"\n        >\n          Send {hint}\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:component"
}