{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt input",
  "description": "A chat input that grows with the text, takes files, and turns into a stop button while the agent writes.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/prompt-input/prompt-input.tsx",
      "content": "import { ArrowUp, Paperclip, Square, X } from 'lucide-react'\nimport { useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type PromptInputProps = {\n  /** Called with the trimmed text and the attached files. */\n  onSubmit: (text: string, files: File[]) => void\n  /** Called when the visitor presses stop while a reply is being written. */\n  onStop?: () => void\n  /** True while a reply is being written. The send button turns into a stop button. */\n  busy?: boolean\n  placeholder?: string\n  /** Accessible name for the text field. */\n  label?: string\n  /** Lets the visitor attach files. The value is passed to the file input's accept. */\n  accept?: string\n  /** Optional choices next to the buttons, e.g. a model picker. */\n  footer?: ReactNode\n  maxRows?: number\n  className?: string\n}\n\nexport function PromptInput({\n  onSubmit,\n  onStop,\n  busy = false,\n  placeholder = 'Ask anything',\n  label = 'Message',\n  accept,\n  footer,\n  maxRows = 8,\n  className,\n}: PromptInputProps) {\n  const [text, setText] = useState('')\n  const [files, setFiles] = useState<File[]>([])\n  const area = useRef<HTMLTextAreaElement>(null)\n  const picker = useRef<HTMLInputElement>(null)\n  const hintId = useId()\n\n  // Grow with the text up to maxRows, then scroll.\n  useLayoutEffect(() => {\n    const el = area.current\n    if (!el) return\n    el.style.height = 'auto'\n    const line = parseFloat(getComputedStyle(el).lineHeight) || 24\n    el.style.height = `${Math.min(el.scrollHeight, line * maxRows + 16)}px`\n  }, [text, maxRows])\n\n  const canSend = !busy && (text.trim().length > 0 || files.length > 0)\n\n  function send(e?: { preventDefault(): void }) {\n    e?.preventDefault()\n    if (!canSend) return\n    onSubmit(text.trim(), files)\n    setText('')\n    setFiles([])\n    area.current?.focus()\n  }\n\n  function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {\n    // Enter sends, Shift+Enter makes a new line. Never send while an IME is composing.\n    if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n      e.preventDefault()\n      send()\n    }\n  }\n\n  return (\n    <form\n      onSubmit={send}\n      className={cn(\n        'rounded-2xl border bg-card p-2 shadow-sm transition-[border-color,box-shadow] duration-150',\n        'focus-within:border-ring focus-within:shadow-[0_0_0_3px_color-mix(in_oklch,var(--color-ring)_18%,transparent)]',\n        className,\n      )}\n    >\n      {files.length > 0 && (\n        <ul className=\"flex flex-wrap gap-1.5 px-1 pt-1 pb-2\" aria-label=\"Attached files\">\n          {files.map((f, i) => (\n            <li key={`${f.name}-${i}`} className=\"flex items-center gap-1 rounded-lg bg-muted py-1 pr-1 pl-2.5 text-sm\">\n              <span className=\"max-w-48 truncate\">{f.name}</span>\n              <button\n                type=\"button\"\n                onClick={() => setFiles((all) => all.filter((_, j) => j !== i))}\n                className=\"grid size-7 place-items-center rounded-md text-muted-foreground hover:bg-background hover:text-foreground\"\n                aria-label={`Remove ${f.name}`}\n              >\n                <X className=\"size-3.5\" aria-hidden />\n              </button>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      <textarea\n        ref={area}\n        rows={1}\n        value={text}\n        onChange={(e) => setText(e.target.value)}\n        onKeyDown={onKeyDown}\n        placeholder={placeholder}\n        aria-label={label}\n        aria-describedby={hintId}\n        className=\"block w-full resize-none bg-transparent px-2 py-2.5 text-base leading-6 outline-none placeholder:text-muted-foreground focus-visible:outline-none\"\n      />\n      <span id={hintId} className=\"sr-only\">\n        Enter sends. Shift and Enter makes a new line.\n      </span>\n\n      <div className=\"flex items-center gap-2 pt-1\">\n        {accept !== undefined && (\n          <>\n            <input\n              ref={picker}\n              type=\"file\"\n              multiple\n              accept={accept}\n              className=\"sr-only\"\n              tabIndex={-1}\n              aria-hidden\n              onChange={(e) => {\n                const picked = Array.from(e.target.files ?? [])\n                setFiles((all) => [...all, ...picked])\n                e.target.value = ''\n              }}\n            />\n            <button\n              type=\"button\"\n              onClick={() => picker.current?.click()}\n              className=\"grid size-11 place-items-center rounded-xl text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n              aria-label=\"Attach files\"\n            >\n              <Paperclip className=\"size-5\" aria-hidden />\n            </button>\n          </>\n        )}\n        <div className=\"flex min-w-0 flex-1 items-center gap-2\">{footer}</div>\n\n        {busy ? (\n          <button\n            type=\"button\"\n            onClick={onStop}\n            className=\"grid size-11 place-items-center rounded-xl bg-foreground text-background transition-transform duration-150 ease-out-quint active:scale-95\"\n            aria-label=\"Stop\"\n          >\n            <Square className=\"size-4 fill-current\" aria-hidden />\n          </button>\n        ) : (\n          <button\n            type=\"submit\"\n            disabled={!canSend}\n            className={cn(\n              'grid size-11 place-items-center rounded-xl bg-primary text-primary-foreground',\n              'transition-[transform,opacity] duration-150 ease-out-quint active:scale-95',\n              'disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100',\n            )}\n            aria-label=\"Send\"\n          >\n            <ArrowUp className=\"size-5\" aria-hidden />\n          </button>\n        )}\n      </div>\n    </form>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "agent"
  ],
  "type": "registry:component"
}