{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt Input",
  "description": "A growing composer that sends on Enter and becomes a stop button while streaming.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/prompt-input/prompt-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUp, Square } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type PromptInputStatus = \"ready\" | \"streaming\"\n\nexport type PromptInputProps = Omit<\n  React.HTMLAttributes<HTMLFormElement>,\n  \"onSubmit\" | \"children\"\n> & {\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  onSubmit?: (value: string) => void\n  onStop?: () => void\n  status?: PromptInputStatus\n  placeholder?: string\n  label?: string\n  disabled?: boolean\n  maxRows?: number\n  attachments?: React.ReactNode\n  actions?: React.ReactNode\n  submitLabel?: string\n  stopLabel?: string\n}\n\nconst LINE_HEIGHT_FALLBACK = 20\n\nexport function PromptInput({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onSubmit,\n  onStop,\n  status = \"ready\",\n  placeholder = \"Ask anything…\",\n  label = \"Message\",\n  disabled = false,\n  maxRows = 8,\n  attachments,\n  actions,\n  submitLabel = \"Send message\",\n  stopLabel = \"Stop generating\",\n  className,\n  ...formProps\n}: PromptInputProps) {\n  const fieldId = React.useId()\n  const fieldRef = React.useRef<HTMLTextAreaElement>(null)\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n\n  const text = value ?? uncontrolled\n  const streaming = status === \"streaming\"\n  const canSend = text.trim().length > 0 && !disabled\n\n  const setText = (next: string) => {\n    if (value === undefined) setUncontrolled(next)\n    onValueChange?.(next)\n  }\n\n  // Grow with the content up to maxRows, then scroll inside the field.\n  React.useEffect(() => {\n    const field = fieldRef.current\n    if (!field) return\n\n    field.style.height = \"auto\"\n\n    const styles = window.getComputedStyle(field)\n    const lineHeight =\n      Number.parseFloat(styles.lineHeight) || LINE_HEIGHT_FALLBACK\n    const padding =\n      Number.parseFloat(styles.paddingTop) +\n      Number.parseFloat(styles.paddingBottom)\n\n    const max = lineHeight * maxRows + padding\n\n    field.style.height = `${Math.min(field.scrollHeight, max)}px`\n    field.style.overflowY = field.scrollHeight > max ? \"auto\" : \"hidden\"\n  }, [text, maxRows])\n\n  const submit = () => {\n    if (!canSend) return\n    onSubmit?.(text.trim())\n    if (value === undefined) setUncontrolled(\"\")\n  }\n\n  return (\n    <form\n      data-slot=\"prompt-input\"\n      data-status={status}\n      className={cn(\n        \"border-border bg-card focus-within:ring-ring/40 rounded-[calc(var(--radius)+0.35rem)] border transition-[box-shadow,border-color] duration-150 focus-within:ring-2 motion-reduce:transition-none\",\n        className\n      )}\n      onSubmit={(event) => {\n        event.preventDefault()\n        submit()\n      }}\n      {...formProps}\n    >\n      {attachments ? (\n        <div\n          data-slot=\"prompt-input-attachments\"\n          className=\"border-border border-b px-3 py-2\"\n        >\n          {attachments}\n        </div>\n      ) : null}\n\n      <label className=\"sr-only\" htmlFor={fieldId}>\n        {label}\n      </label>\n      <textarea\n        ref={fieldRef}\n        id={fieldId}\n        data-slot=\"prompt-input-field\"\n        rows={1}\n        disabled={disabled}\n        placeholder={placeholder}\n        value={text}\n        className=\"placeholder:text-muted-foreground block w-full resize-none bg-transparent px-3.5 py-3 text-sm leading-relaxed outline-none disabled:opacity-60\"\n        onChange={(event) => setText(event.target.value)}\n        onKeyDown={(event) => {\n          // Enter sends, Shift+Enter starts a new line. During composition of\n          // an IME candidate Enter belongs to the IME, never to the form.\n          if (event.key !== \"Enter\" || event.shiftKey) return\n          if (event.nativeEvent.isComposing) return\n\n          event.preventDefault()\n          submit()\n        }}\n      />\n\n      <div\n        data-slot=\"prompt-input-toolbar\"\n        className=\"flex items-center gap-2 px-2 pb-2\"\n      >\n        {actions}\n\n        {streaming ? (\n          <button\n            type=\"button\"\n            data-slot=\"prompt-input-stop\"\n            aria-label={stopLabel}\n            className=\"bg-foreground text-background focus-visible:ring-ring ml-auto inline-flex size-9 shrink-0 items-center justify-center rounded-full transition-opacity duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n            onClick={onStop}\n          >\n            <Square aria-hidden=\"true\" size={13} fill=\"currentColor\" />\n          </button>\n        ) : (\n          <button\n            type=\"submit\"\n            data-slot=\"prompt-input-submit\"\n            aria-label={submitLabel}\n            disabled={!canSend}\n            className=\"bg-foreground text-background focus-visible:ring-ring ml-auto inline-flex size-9 shrink-0 items-center justify-center rounded-full transition-opacity duration-150 focus-visible:ring-2 focus-visible:outline-none disabled:opacity-40 motion-reduce:transition-none\"\n          >\n            <ArrowUp aria-hidden=\"true\" size={16} />\n          </button>\n        )}\n      </div>\n    </form>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "chat",
    "input",
    "form",
    "agent"
  ],
  "type": "registry:ui"
}