{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt Input",
  "description": "The composer for prompting an agent: autosize, attachments, model picker, stop.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/prompt-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowUpIcon, PaperclipIcon, SquareIcon, XIcon } from \"lucide-react\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptInputProps = Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit?: (message: { text: string; files: File[] }) => void;\n  /** While streaming, the send button becomes Stop. */\n  status?: \"idle\" | \"streaming\";\n  onStop?: () => void;\n  placeholder?: string;\n  /** Extra controls in the toolbar, e.g. a ModelSelector. */\n  toolbar?: React.ReactNode;\n  /** Allow attaching files (button, paste and drop). */\n  attachments?: boolean;\n  disabled?: boolean;\n};\n\n/**\n * The composer for talking to an agent. It grows with the text; Enter sends and Shift+Enter breaks a line; files\n * attach by button, paste or drop; while the agent works, Send turns into Stop (Esc). On send, a signal line\n * sweeps the top edge.\n */\nfunction PromptInput({ onSubmit, status = \"idle\", onStop, placeholder = \"Ask anything…\", toolbar, attachments = true, disabled, className, ...props }: PromptInputProps) {\n  const [text, setText] = React.useState(\"\");\n  const [files, setFiles] = React.useState<File[]>([]);\n  const [over, setOver] = React.useState(false);\n  const form = React.useRef<HTMLFormElement>(null);\n  const picker = React.useRef<HTMLInputElement>(null);\n  const streaming = status === \"streaming\";\n  const canSend = !disabled && !streaming && (text.trim().length > 0 || files.length > 0);\n\n  const send = () => {\n    if (!canSend) return;\n    onSubmit?.({ text: text.trim(), files });\n    setText(\"\");\n    setFiles([]);\n    const line = form.current?.querySelector('[data-slot=\"prompt-sweep\"]');\n    if (line && form.current && motionScale(form.current)) gsap.fromTo(line, { scaleX: 0, opacity: 1, transformOrigin: \"left\" }, { scaleX: 1, duration: 0.5, ease: \"cue\", onComplete: () => void gsap.to(line, { opacity: 0, duration: 0.3 }) });\n  };\n\n  React.useEffect(() => {\n    if (!streaming) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && onStop?.();\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [streaming, onStop]);\n\n  return (\n    <form\n      ref={form}\n      data-slot=\"prompt-input\"\n      data-over={over || undefined}\n      onSubmit={(e) => {\n        e.preventDefault();\n        send();\n      }}\n      onDragOver={(e) => attachments && (e.preventDefault(), setOver(true))}\n      onDragLeave={() => setOver(false)}\n      onDrop={(e) => {\n        if (!attachments) return;\n        e.preventDefault();\n        setOver(false);\n        setFiles((f) => [...f, ...Array.from(e.dataTransfer.files)]);\n      }}\n      className={cn(\n        \"relative grid gap-2 overflow-hidden rounded-2xl border border-input bg-card p-2 shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/20 data-[over]:border-ring data-[over]:bg-signal/5\",\n        className,\n      )}\n      {...props}\n    >\n      <span data-slot=\"prompt-sweep\" aria-hidden=\"true\" className=\"absolute inset-x-0 top-0 h-0.5 bg-signal opacity-0\" />\n      {files.length > 0 && (\n        <ul className=\"flex flex-wrap gap-1.5 px-1 pt-1\">\n          {files.map((f, i) => (\n            <li key={`${f.name}-${i}`} className=\"inline-flex h-7 max-w-48 items-center gap-1.5 rounded-lg border border-border bg-muted/60 ps-2 pe-1 text-xs\">\n              <PaperclipIcon className=\"size-3 shrink-0 text-muted-foreground\" />\n              <span className=\"truncate\">{f.name}</span>\n              <button type=\"button\" aria-label={`Remove ${f.name}`} onClick={() => setFiles((all) => all.filter((_, j) => j !== i))} className=\"relative after:absolute after:-inset-2 grid size-5 place-items-center rounded-md text-muted-foreground hover:bg-foreground/10 hover:text-foreground\">\n                <XIcon className=\"size-3\" />\n              </button>\n            </li>\n          ))}\n        </ul>\n      )}\n      <textarea\n        value={text}\n        rows={1}\n        disabled={disabled}\n        placeholder={placeholder}\n        aria-label=\"Message\"\n        onChange={(e) => setText(e.target.value)}\n        onPaste={(e) => {\n          if (attachments && e.clipboardData.files.length) setFiles((f) => [...f, ...Array.from(e.clipboardData.files)]);\n        }}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n            e.preventDefault();\n            send();\n          }\n        }}\n        className=\"max-h-48 min-h-11 w-full resize-none bg-transparent px-2 py-1.5 text-sm outline-none placeholder:text-muted-foreground/80 [field-sizing:content]\"\n      />\n      <div className=\"flex items-center gap-1\">\n        {attachments && (\n          <>\n            <button\n              type=\"button\"\n              aria-label=\"Attach files\"\n              onClick={() => picker.current?.click()}\n              className=\"grid size-8 place-items-center rounded-lg text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50\"\n            >\n              <PaperclipIcon className=\"size-4\" />\n            </button>\n            <input ref={picker} type=\"file\" multiple hidden onChange={(e) => e.target.files && setFiles((f) => [...f, ...Array.from(e.target.files!)])} />\n          </>\n        )}\n        {toolbar}\n        <span className=\"ms-auto hidden items-center gap-1 pe-1 text-[0.6875rem] text-muted-foreground sm:flex\">\n          {streaming ? (\n            <>\n              <kbd className=\"cue-kbd\">esc</kbd> to stop\n            </>\n          ) : (\n            <>\n              <kbd className=\"cue-kbd\">⏎</kbd> to send\n            </>\n          )}\n        </span>\n        {streaming ? (\n          <button\n            type=\"button\"\n            onClick={onStop}\n            aria-label=\"Stop generating\"\n            className=\"relative grid size-8 place-items-center rounded-full bg-primary text-primary-foreground outline-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-95\"\n          >\n            <span aria-hidden=\"true\" className=\"absolute inset-0 animate-ping rounded-full bg-signal/40 motion-reduce:hidden\" />\n            <SquareIcon className=\"relative size-3 fill-current\" />\n          </button>\n        ) : (\n          <button\n            type=\"submit\"\n            disabled={!canSend}\n            aria-label=\"Send message\"\n            className=\"grid size-8 place-items-center rounded-full bg-primary text-primary-foreground outline-none transition-[opacity,scale] duration-150 focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-95 disabled:opacity-30\"\n          >\n            <ArrowUpIcon className=\"size-4\" />\n          </button>\n        )}\n      </div>\n    </form>\n  );\n}\n\nexport { PromptInput };\nexport type { PromptInputProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/prompt-input",
  "categories": [
    "agents"
  ],
  "type": "registry:ui"
}