{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt Input",
  "description": "A chat composer with an auto-growing textarea, file attachments and a send button that turns into Stop while a reply streams.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/tealmac/prompt-input.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowUp,\n  FileText,\n  Image as ImageIcon,\n  Paperclip,\n  Square,\n  X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\ntype PromptInputStatus = \"idle\" | \"streaming\"\n\ntype PromptInputContextValue = {\n  value: string\n  setValue: (value: string) => void\n  files: File[]\n  setFiles: (files: File[]) => void\n  status: PromptInputStatus\n  disabled: boolean\n  canSubmit: boolean\n  submit: () => void\n  stop?: () => void\n  textareaRef: React.RefObject<HTMLTextAreaElement | null>\n}\n\nconst PromptInputContext = React.createContext<PromptInputContextValue | null>(\n  null\n)\n\nfunction usePromptInput() {\n  const ctx = React.useContext(PromptInputContext)\n  if (!ctx)\n    throw new Error(\"PromptInput parts must be rendered inside <PromptInput>.\")\n  return ctx\n}\n\nfunction useControllable<T>(\n  prop: T | undefined,\n  defaultValue: T,\n  onChange?: (value: T) => void\n) {\n  const [internal, setInternal] = React.useState(defaultValue)\n  const controlled = prop !== undefined\n  const value = controlled ? prop : internal\n  const onChangeRef = React.useRef(onChange)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n  })\n  const setValue = React.useCallback(\n    (next: T) => {\n      if (!controlled) setInternal(next)\n      onChangeRef.current?.(next)\n    },\n    [controlled]\n  )\n  return [value, setValue] as const\n}\n\ntype PromptInputProps = Omit<\n  React.ComponentProps<\"form\">,\n  \"onSubmit\" | \"defaultValue\"\n> & {\n  /** Called with the trimmed text and attached files on Enter or the send button. Uncontrolled inputs clear themselves afterwards. */\n  onSubmit?: (value: string, files: File[]) => void\n  /** Controlled text. Clear it yourself in onSubmit. */\n  value?: string\n  /** Initial text when uncontrolled. */\n  defaultValue?: string\n  /** Called on every edit. */\n  onValueChange?: (value: string) => void\n  /** Controlled attachments. */\n  files?: File[]\n  /** Called when files are attached or removed. Nothing is uploaded; that is your job. */\n  onFilesChange?: (files: File[]) => void\n  /** \"streaming\" turns the send button into a Stop button and blocks new submissions. */\n  status?: PromptInputStatus\n  /** Called by the Stop button while streaming. */\n  onStop?: () => void\n  /** Disables typing, attaching and sending. */\n  disabled?: boolean\n}\n\nfunction PromptInput({\n  onSubmit,\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  files: filesProp,\n  onFilesChange,\n  status = \"idle\",\n  onStop,\n  disabled = false,\n  className,\n  children,\n  ref,\n  ...props\n}: PromptInputProps) {\n  const formRef = React.useRef<HTMLFormElement>(null)\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n  const setFormRef = React.useCallback(\n    (node: HTMLFormElement | null) => {\n      formRef.current = node\n      if (typeof ref === \"function\") ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref]\n  )\n\n  // Clicking the padding focuses the textarea, like a single field.\n  React.useEffect(() => {\n    const form = formRef.current\n    if (!form) return\n    const onMouseDown = (event: MouseEvent) => {\n      if (event.target !== form) return\n      event.preventDefault()\n      textareaRef.current?.focus()\n    }\n    form.addEventListener(\"mousedown\", onMouseDown)\n    return () => form.removeEventListener(\"mousedown\", onMouseDown)\n  }, [])\n\n  const [value, setValue] = useControllable(\n    valueProp,\n    defaultValue,\n    onValueChange\n  )\n  const [files, setFiles] = useControllable<File[]>(\n    filesProp,\n    [],\n    onFilesChange\n  )\n  const canSubmit =\n    !disabled &&\n    status !== \"streaming\" &&\n    (value.trim().length > 0 || files.length > 0)\n\n  const submit = React.useCallback(() => {\n    if (!canSubmit) return\n    onSubmit?.(value.trim(), files)\n    if (valueProp === undefined) setValue(\"\")\n    if (filesProp === undefined) setFiles([])\n  }, [\n    canSubmit,\n    onSubmit,\n    value,\n    files,\n    valueProp,\n    filesProp,\n    setValue,\n    setFiles,\n  ])\n\n  const ctx = React.useMemo<PromptInputContextValue>(\n    () => ({\n      value,\n      setValue,\n      files,\n      setFiles,\n      status,\n      disabled,\n      canSubmit,\n      submit,\n      stop: onStop,\n      textareaRef,\n    }),\n    [\n      value,\n      setValue,\n      files,\n      setFiles,\n      status,\n      disabled,\n      canSubmit,\n      submit,\n      onStop,\n    ]\n  )\n\n  return (\n    <PromptInputContext.Provider value={ctx}>\n      <form\n        ref={setFormRef}\n        data-slot=\"prompt-input\"\n        data-status={status}\n        data-disabled={disabled || undefined}\n        className={cn(\n          \"border-input bg-card focus-within:border-ring focus-within:ring-ring/50 flex w-full cursor-text flex-col rounded-xl border shadow-xs transition-[border-color,box-shadow] duration-150 focus-within:ring-[3px] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-60\",\n          className\n        )}\n        onSubmit={(event) => {\n          event.preventDefault()\n          submit()\n        }}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <PromptInputAttachments />\n            <PromptInputTextarea />\n            <PromptInputToolbar>\n              {onFilesChange ? <PromptInputAttachButton /> : null}\n              <PromptInputSubmit />\n            </PromptInputToolbar>\n          </>\n        )}\n      </form>\n    </PromptInputContext.Provider>\n  )\n}\n\ntype PromptInputTextareaProps = Omit<\n  React.ComponentProps<\"textarea\">,\n  \"value\" | \"defaultValue\"\n> & {\n  /** Height (px) the textarea grows to before it scrolls. */\n  maxHeight?: number\n}\n\nfunction PromptInputTextarea({\n  maxHeight = 200,\n  placeholder,\n  className,\n  onKeyDown,\n  onChange,\n  ref,\n  style,\n  ...props\n}: PromptInputTextareaProps) {\n  placeholder ??= \"Send a message…\"\n  const { value, setValue, submit, disabled, textareaRef } = usePromptInput()\n\n  const setRefs = React.useCallback(\n    (node: HTMLTextAreaElement | null) => {\n      textareaRef.current = node\n      if (typeof ref === \"function\") ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref, textareaRef]\n  )\n\n  // Grow with the content (no dependency): measure from auto height each change.\n  React.useLayoutEffect(() => {\n    const node = textareaRef.current\n    if (!node) return\n    node.style.height = \"auto\"\n    node.style.height = `${Math.min(node.scrollHeight, maxHeight)}px`\n    node.style.overflowY = node.scrollHeight > maxHeight ? \"auto\" : \"hidden\"\n  }, [value, maxHeight, textareaRef])\n\n  return (\n    <textarea\n      ref={setRefs}\n      data-slot=\"prompt-input-textarea\"\n      rows={1}\n      aria-label={props[\"aria-labelledby\"] ? undefined : \"Message\"}\n      placeholder={placeholder}\n      disabled={disabled}\n      value={value}\n      onChange={(event) => {\n        onChange?.(event)\n        setValue(event.target.value)\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (event.defaultPrevented) return\n        // Enter sends; Shift+Enter inserts a newline; ignore Enter that confirms an IME composition.\n        if (\n          event.key === \"Enter\" &&\n          !event.shiftKey &&\n          !event.nativeEvent.isComposing &&\n          event.keyCode !== 229\n        ) {\n          event.preventDefault()\n          submit()\n        }\n      }}\n      style={{ maxHeight, ...style }}\n      className={cn(\n        \"placeholder:text-muted-foreground block min-h-11 w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-sm leading-6 outline-none disabled:cursor-not-allowed\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype PromptInputToolbarProps = React.ComponentProps<\"div\">\n\n/** The bottom row: attach button, model pickers or other controls on the left, send/stop on the right. */\nfunction PromptInputToolbar({ className, ...props }: PromptInputToolbarProps) {\n  return (\n    <div\n      data-slot=\"prompt-input-toolbar\"\n      className={cn(\n        \"flex min-w-0 flex-wrap items-center gap-1 px-2 pb-2\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype PromptInputSubmitProps = Omit<\n  React.ComponentProps<typeof Button>,\n  \"type\" | \"onClick\"\n> & {\n  /** Accessible name of the send button. */\n  submitLabel?: string\n  /** Accessible name of the stop button. */\n  stopLabel?: string\n}\n\nfunction PromptInputSubmit({\n  submitLabel,\n  stopLabel,\n  className,\n  children,\n  ...props\n}: PromptInputSubmitProps) {\n  submitLabel ??= \"Send message\"\n  stopLabel ??= \"Stop generating\"\n  const { status, canSubmit, stop, disabled } = usePromptInput()\n  const streaming = status === \"streaming\"\n  return (\n    <Button\n      data-slot=\"prompt-input-submit\"\n      data-status={status}\n      type={streaming ? \"button\" : \"submit\"}\n      size=\"icon\"\n      aria-label={streaming ? stopLabel : submitLabel}\n      title={streaming ? stopLabel : submitLabel}\n      disabled={streaming ? disabled || !stop : !canSubmit}\n      onClick={streaming ? () => stop?.() : undefined}\n      className={cn(\"ms-auto size-8 rounded-full\", className)}\n      {...props}\n    >\n      {children ??\n        (streaming ? <Square className=\"size-3 fill-current\" /> : <ArrowUp />)}\n    </Button>\n  )\n}\n\ntype PromptInputAttachButtonProps = Omit<\n  React.ComponentProps<typeof Button>,\n  \"type\" | \"onClick\"\n> & {\n  /** File types to accept, as for <input type=\"file\" accept>. */\n  accept?: string\n  /** Allow picking several files at once. */\n  multiple?: boolean\n  /** Accessible name of the icon button. */\n  label?: string\n}\n\nfunction PromptInputAttachButton({\n  accept,\n  multiple = true,\n  label,\n  className,\n  children,\n  ...props\n}: PromptInputAttachButtonProps) {\n  label ??= \"Attach files\"\n  const { files, setFiles, disabled } = usePromptInput()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  return (\n    <>\n      <input\n        ref={inputRef}\n        type=\"file\"\n        tabIndex={-1}\n        aria-hidden=\"true\"\n        className=\"hidden\"\n        accept={accept}\n        multiple={multiple}\n        disabled={disabled}\n        onChange={(event) => {\n          const picked = Array.from(event.target.files ?? [])\n          if (picked.length)\n            setFiles(multiple ? [...files, ...picked] : picked.slice(0, 1))\n          event.target.value = \"\"\n        }}\n      />\n      <Button\n        data-slot=\"prompt-input-attach\"\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"icon\"\n        aria-label={label}\n        title={label}\n        disabled={disabled}\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"text-muted-foreground hover:text-foreground size-8\",\n          className\n        )}\n        {...props}\n      >\n        {children ?? <Paperclip />}\n      </Button>\n    </>\n  )\n}\n\nfunction formatBytes(bytes: number) {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\ntype PromptInputAttachmentsProps = React.ComponentProps<\"ul\">\n\n/** Removable chips for the attached files. Renders nothing when there are none. */\nfunction PromptInputAttachments({\n  className,\n  ...props\n}: PromptInputAttachmentsProps) {\n  const { files, setFiles, disabled } = usePromptInput()\n  if (!files.length) return null\n  return (\n    <ul\n      data-slot=\"prompt-input-attachments\"\n      aria-label=\"Attachments\"\n      className={cn(\"flex flex-wrap gap-1.5 px-2.5 pt-2.5\", className)}\n      {...props}\n    >\n      {files.map((file, index) => {\n        const Icon = file.type.startsWith(\"image/\") ? ImageIcon : FileText\n        return (\n          <li\n            key={`${file.name}-${file.size}-${file.lastModified}-${index}`}\n            data-slot=\"prompt-input-attachment\"\n            className=\"bg-muted/50 flex h-8 max-w-full items-center gap-1.5 rounded-md border ps-2 pe-1 text-xs\"\n          >\n            <Icon\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground size-3.5 shrink-0\"\n            />\n            <span className=\"max-w-40 truncate font-medium\">{file.name}</span>\n            <span className=\"text-muted-foreground shrink-0 font-mono text-[11px] tabular-nums\">\n              {formatBytes(file.size)}\n            </span>\n            <button\n              type=\"button\"\n              aria-label={`Remove ${file.name}`}\n              disabled={disabled}\n              onClick={() => setFiles(files.filter((_, i) => i !== index))}\n              className=\"text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring/50 flex size-6 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50\"\n            >\n              <X aria-hidden=\"true\" className=\"size-3.5\" />\n            </button>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nexport {\n  PromptInput,\n  PromptInputTextarea,\n  PromptInputToolbar,\n  PromptInputSubmit,\n  PromptInputAttachButton,\n  PromptInputAttachments,\n  usePromptInput,\n  type PromptInputProps,\n  type PromptInputTextareaProps,\n  type PromptInputToolbarProps,\n  type PromptInputSubmitProps,\n  type PromptInputAttachButtonProps,\n  type PromptInputAttachmentsProps,\n  type PromptInputStatus,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}