{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"prompt-input","type":"registry:component","title":"Prompt Input","description":"A chat composer that grows with its content, submits on Enter, and crossfades its send button into a stop button while a reply streams.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/prompt-input.tsx","type":"registry:component","target":"@components/motion/prompt-input.tsx","content":"\"use client\";\n// easeui.dev/components/agents/prompt-input\n\nimport { ArrowUp, Square } from \"lucide-react\";\nimport { type KeyboardEvent, type TextareaHTMLAttributes, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PromptInputProps\n  extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"onSubmit\"> {\n  /** Controlled value. */\n  value?: string;\n  /** Starting value when uncontrolled. Default \"\". */\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Called with the trimmed text on Enter or the send button. */\n  onSubmit: (value: string) => void;\n  /** Swaps the send button for a stop button. Default false. */\n  loading?: boolean;\n  /** Called from the stop button while loading. */\n  onStop?: () => void;\n  className?: string;\n}\n\n// Same crossfade CopyButton uses, so the button never changes size.\nconst SWAP = \"col-start-1 row-start-1 transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none\";\nconst SHOWN = \"scale-100 opacity-100\";\nconst HIDDEN = \"scale-50 opacity-0\";\n\n/**\n * A chat composer: a textarea that grows with its content up to a scrollable\n * cap, and a send button beside it. Enter submits, Shift+Enter starts a new\n * line, and the button crossfades into a stop button while a reply streams.\n */\nexport function PromptInput({\n  value,\n  defaultValue = \"\",\n  onChange,\n  onSubmit,\n  loading = false,\n  onStop,\n  disabled = false,\n  placeholder = \"Message...\",\n  className,\n  ...props\n}: PromptInputProps) {\n  const [uncontrolled, setUncontrolled] = useState(defaultValue);\n  const isControlled = value !== undefined;\n  const text = isControlled ? value : uncontrolled;\n\n  const setValue = (next: string) => {\n    if (!isControlled) setUncontrolled(next);\n    onChange?.(next);\n  };\n\n  const submit = () => {\n    const trimmed = text.trim();\n    if (!trimmed || loading || disabled) return;\n    onSubmit(trimmed);\n    if (!isControlled) setUncontrolled(\"\");\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    if (event.key === \"Enter\" && !event.shiftKey && !loading && !disabled) {\n      event.preventDefault();\n      submit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex items-end gap-2 rounded-2xl bg-card p-2 pl-3.5 shadow-[0_0_0_1px_var(--border-strong)] outline-none transition-shadow duration-150 ease-out\",\n        \"focus-within:shadow-[0_0_0_2px_var(--accent)]\",\n        className,\n      )}\n    >\n      <textarea\n        rows={1}\n        disabled={disabled}\n        placeholder={placeholder}\n        value={text}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={onKeyDown}\n        className={cn(\n          \"max-h-48 flex-1 resize-none bg-transparent py-1.5 text-base text-foreground outline-none sm:text-sm\",\n          \"[field-sizing:content]\",\n          \"placeholder:text-muted-foreground\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n        )}\n        {...props}\n      />\n      <button\n        type=\"button\"\n        aria-label={loading ? \"Stop\" : \"Send\"}\n        disabled={loading ? false : disabled || !text.trim()}\n        onClick={() => (loading ? onStop?.() : submit())}\n        className={cn(\n          \"relative inline-flex h-9 w-9 shrink-0 touch-manipulation items-center justify-center rounded-full bg-foreground text-background outline-none\",\n          \"transition-[background-color,opacity] duration-150 ease-out hover:bg-foreground/90\",\n          \"focus-visible:ring-2 focus-visible:ring-foreground/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:pointer-events-none disabled:opacity-40\",\n        )}\n      >\n        <span aria-hidden=\"true\" className=\"grid place-items-center\">\n          <ArrowUp className={cn(SWAP, \"h-4 w-4\", loading ? HIDDEN : SHOWN)} />\n          <Square className={cn(SWAP, \"h-3 w-3 fill-current\", loading ? SHOWN : HIDDEN)} />\n        </span>\n      </button>\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}