{
  "schemaVersion": 1,
  "target": "react",
  "name": "prompt-composer",
  "version": "0.2.0",
  "category": "agent",
  "title": "Prompt Composer",
  "description": "A prompt box that grows with its text: Enter sends, Shift+Enter adds a line, and Stop replaces Send during a run.",
  "tags": [
    "agent",
    "chat",
    "input",
    "prompt",
    "composer"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "button",
      "version": "0.2.0"
    }
  ],
  "docsPath": "/components/prompt-composer",
  "props": [
    {
      "name": "onSubmit",
      "type": "(prompt: string) => void | Promise<void>",
      "description": "Receives the trimmed prompt; the field clears when it resolves."
    },
    {
      "name": "onStop",
      "type": "() => void",
      "description": "Shown as a Stop button while status is running."
    },
    {
      "name": "status",
      "type": "\"idle\" | \"submitting\" | \"running\"",
      "default": "\"idle\"",
      "description": "Disables sending while busy."
    },
    {
      "name": "placeholder / label / maxLength",
      "type": "string / string / number",
      "description": "Field text, accessible label, and character limit (a counter appears near the limit)."
    }
  ],
  "accessibility": [
    "The field has an accessible label and a hint describing Enter and Shift+Enter; while running, the hint states that the agent is working.",
    "Enter does not send while an input method is composing text.",
    "Send shows a busy state while submitting; Stop replaces it during a run."
  ],
  "states": [
    "idle",
    "submitting",
    "running"
  ],
  "files": [
    {
      "path": "agent/prompt-composer.tsx",
      "content": "\"use client\";\n\nimport { type FormEvent, type KeyboardEvent, useId, useState } from \"react\";\nimport { Button } from \"../ui/button\";\n\nexport type ComposerStatus = \"idle\" | \"submitting\" | \"running\";\n\nexport interface PromptComposerProps {\n  /** Called with the trimmed prompt. The field clears when it resolves. */\n  onSubmit: (prompt: string) => void | Promise<void>;\n  /** Shown as a Stop button while the agent is running. */\n  onStop?: () => void;\n  status?: ComposerStatus;\n  placeholder?: string;\n  /** Accessible label for the field. */\n  label?: string;\n  maxLength?: number;\n  defaultValue?: string;\n  className?: string;\n}\n\nconst ease = \"ease-[cubic-bezier(0.23,1,0.32,1)]\";\n\n/** A prompt box: Enter sends, Shift+Enter adds a line, and Stop replaces Send while a run is active. */\nexport function PromptComposer({\n  onSubmit,\n  onStop,\n  status = \"idle\",\n  placeholder = \"Ask the agent to do something…\",\n  label = \"Message the agent\",\n  maxLength = 4000,\n  defaultValue = \"\",\n  className,\n}: PromptComposerProps) {\n  const [value, setValue] = useState(defaultValue);\n  const id = useId();\n  const busy = status !== \"idle\";\n  const canSend = value.trim().length > 0 && !busy;\n  const nearLimit = value.length > maxLength * 0.9;\n\n  async function submit(event?: FormEvent) {\n    event?.preventDefault();\n    if (!canSend) return;\n    const prompt = value.trim();\n    await onSubmit(prompt);\n    setValue(\"\");\n  }\n\n  function onKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {\n    // Ignore Enter while an input method is composing text (e.g. Japanese, Chinese).\n    if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n      event.preventDefault();\n      void submit();\n    }\n  }\n\n  return (\n    <form\n      onSubmit={submit}\n      data-status={status}\n      className={[\n        \"rounded-xl border border-zinc-950/10 bg-white shadow-[0_1px_2px_rgb(0_0_0/0.04)]\",\n        \"transition-[border-color,box-shadow] duration-150 ease-out motion-reduce:transition-none\",\n        \"hover:border-zinc-950/15 focus-within:border-zinc-950/25 focus-within:shadow-[0_0_0_3px_rgb(14_165_233/0.16)]\",\n        \"dark:border-white/10 dark:bg-black dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)] dark:hover:border-white/20\",\n        \"dark:focus-within:border-white/30 dark:focus-within:shadow-[0_0_0_3px_rgb(56_189_248/0.18),inset_0_1px_0_rgb(255_255_255/0.06)]\",\n        className,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <label htmlFor={id} className=\"sr-only\">\n        {label}\n      </label>\n      <textarea\n        id={id}\n        value={value}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={onKeyDown}\n        placeholder={placeholder}\n        maxLength={maxLength}\n        rows={1}\n        aria-describedby={`${id}-hint`}\n        className=\"block max-h-48 min-h-11 w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-sm leading-relaxed text-zinc-950 [field-sizing:content] placeholder:text-zinc-500 focus:outline-none dark:text-white dark:placeholder:text-zinc-500\"\n      />\n      <div className=\"flex min-h-11 items-center justify-between gap-3 py-2 pr-2 pl-3.5\">\n        <p id={`${id}-hint`} className=\"flex min-w-0 items-center gap-2 text-xs text-zinc-500\">\n          {status === \"running\" ? (\n            <span key=\"running\" className={`flex items-center gap-2 text-sky-700 transition-opacity duration-200 ${ease} starting:opacity-0 motion-reduce:transition-none dark:text-sky-400`}>\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" className=\"size-3.5 shrink-0\">\n                <circle cx=\"8\" cy=\"8\" r=\"6.25\" strokeOpacity=\"0.2\" />\n                <path d=\"M8 1.75A6.25 6.25 0 1 1 1.75 8\" className=\"origin-center motion-safe:animate-spin\" />\n              </svg>\n              Agent is working\n            </span>\n          ) : (\n            <span key=\"hint\" className=\"hidden items-center gap-1 sm:flex\">\n              <Key>Enter</Key> to send, <Key>Shift</Key>\n              <span aria-hidden=\"true\">+</span>\n              <Key>Enter</Key> for a new line\n            </span>\n          )}\n          {nearLimit ? (\n            <span className=\"font-mono tabular-nums text-amber-700 dark:text-amber-400\">{maxLength - value.length} characters left</span>\n          ) : null}\n        </p>\n        {status === \"running\" && onStop ? (\n          <Button type=\"button\" size=\"sm\" variant=\"secondary\" onClick={onStop}>\n            <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n              <rect x=\"4.5\" y=\"4.5\" width=\"7\" height=\"7\" rx=\"1.5\" />\n            </svg>\n            Stop\n          </Button>\n        ) : (\n          <Button type=\"submit\" size=\"sm\" loading={status === \"submitting\"} disabled={!canSend && status !== \"submitting\"}>\n            Send\n          </Button>\n        )}\n      </div>\n    </form>\n  );\n}\n\nfunction Key({ children }: { children: string }) {\n  return (\n    <kbd className=\"rounded-[5px] border border-zinc-950/10 px-1 font-sans text-[11px] leading-4 text-zinc-600 dark:border-white/15 dark:text-zinc-400\">\n      {children}\n    </kbd>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { type ComposerStatus, PromptComposer } from \"@/components/seamless/agent/prompt-composer\";\n\nexport default function PromptComposerExample() {\n  const [status, setStatus] = useState<ComposerStatus>(\"idle\");\n  const [last, setLast] = useState<string | null>(null);\n\n  async function send(prompt: string) {\n    setStatus(\"submitting\");\n    await new Promise((resolve) => setTimeout(resolve, 500)); // your API call\n    setLast(prompt);\n    setStatus(\"running\");\n  }\n\n  return (\n    <div className=\"grid w-full max-w-lg gap-2\">\n      <PromptComposer onSubmit={send} onStop={() => setStatus(\"idle\")} status={status} />\n      {last ? <p className=\"text-xs text-zinc-500\">Sent: {last}</p> : null}\n    </div>\n  );\n}\n"
  }
}
