{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt Input",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "Chat composer for AI apps: an auto-growing textarea that submits on Enter (Shift+Enter for newlines), a footer/header tools row, an attachment action menu (add files, take screenshot), model select, hover-card and command-palette slots, and a submit button that turns into a stop button while submitted or streaming when onStop is provided. Handles file attachments via drag-drop, paste, and file dialog with accept/maxFiles/maxFileSize validation, converts blob URLs to data URLs on submit, and can lift its state into an optional PromptInputProvider. Use it as the input at the bottom of any AI SDK useChat conversation. Composes input-group, dropdown-menu, select, tooltip, hover-card, command, spinner.",
  "dependencies": [
    "ai@^7",
    "cn",
    "lucide-react",
    "nanoid@^6"
  ],
  "registryDependencies": [
    "command",
    "dropdown-menu",
    "hover-card",
    "input-group",
    "select",
    "spinner",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/ai/prompt-input.tsx",
      "content": "// Derived from Vercel AI Elements prompt-input.tsx (Apache-2.0, Copyright 2023 Vercel, Inc.).\n// Modified for uifiles: ported from Radix UI to Base UI; dependencies point at @uifiles.\n\"use client\"\n\nimport type { ChatStatus, FileUIPart, SourceDocumentUIPart } from \"ai\"\nimport { cn } from \"cn\"\nimport {\n  CornerDownLeftIcon,\n  ImageIcon,\n  Monitor,\n  PlusIcon,\n  SquareIcon,\n  XIcon,\n} from \"lucide-react\"\nimport { nanoid } from \"nanoid\"\nimport type {\n  ChangeEvent,\n  ChangeEventHandler,\n  ClipboardEventHandler,\n  ComponentProps,\n  FormEvent,\n  FormEventHandler,\n  HTMLAttributes,\n  KeyboardEventHandler,\n  MouseEvent,\n  MouseEventHandler,\n  PropsWithChildren,\n  ReactNode,\n  RefObject,\n} from \"react\"\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@/components/ui/command\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupTextarea,\n} from \"@/components/ui/input-group\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {\n  try {\n    const response = await fetch(url)\n    const blob = await response.blob()\n    // FileReader uses callback-based API, wrapping in Promise is necessary\n    return new Promise((resolve) => {\n      const reader = new FileReader()\n      reader.onloadend = () => resolve(reader.result as string)\n      reader.onerror = () => resolve(null)\n      reader.readAsDataURL(blob)\n    })\n  } catch {\n    return null\n  }\n}\n\nconst captureScreenshot = async (): Promise<File | null> => {\n  if (\n    typeof navigator === \"undefined\" ||\n    !navigator.mediaDevices?.getDisplayMedia\n  ) {\n    return null\n  }\n\n  let stream: MediaStream | null = null\n  const video = document.createElement(\"video\")\n  video.muted = true\n  video.playsInline = true\n\n  try {\n    stream = await navigator.mediaDevices.getDisplayMedia({\n      audio: false,\n      video: true,\n    })\n\n    video.srcObject = stream\n\n    // Video element uses callback-based API, wrapping in Promise is necessary\n    await new Promise<void>((resolve, reject) => {\n      video.onloadedmetadata = () => resolve()\n      video.onerror = () => reject(new Error(\"Failed to load screen stream\"))\n    })\n\n    await video.play()\n\n    const width = video.videoWidth\n    const height = video.videoHeight\n    if (!width || !height) {\n      return null\n    }\n\n    const canvas = document.createElement(\"canvas\")\n    canvas.width = width\n    canvas.height = height\n    const context = canvas.getContext(\"2d\")\n    if (!context) {\n      return null\n    }\n\n    context.drawImage(video, 0, 0, width, height)\n    // canvas.toBlob uses callback-based API, wrapping in Promise is necessary\n    const blob = await new Promise<Blob | null>((resolve) => {\n      canvas.toBlob(resolve, \"image/png\")\n    })\n    if (!blob) {\n      return null\n    }\n\n    const timestamp = new Date()\n      .toISOString()\n      .replaceAll(/[:.]/g, \"-\")\n      .replace(\"T\", \"_\")\n      .replace(\"Z\", \"\")\n\n    return new File([blob], `screenshot-${timestamp}.png`, {\n      lastModified: Date.now(),\n      type: \"image/png\",\n    })\n  } finally {\n    if (stream) {\n      for (const track of stream.getTracks()) {\n        track.stop()\n      }\n    }\n    video.pause()\n    video.srcObject = null\n  }\n}\n\ntype AttachmentFile = FileUIPart & { id: string }\n\nconst toAttachment = (file: File): AttachmentFile => ({\n  filename: file.name,\n  id: nanoid(),\n  mediaType: file.type,\n  type: \"file\",\n  url: URL.createObjectURL(file),\n})\n\nconst revokeObjectUrls = (files: Pick<FileUIPart, \"url\">[]) => {\n  for (const file of files) {\n    if (file.url) {\n      URL.revokeObjectURL(file.url)\n    }\n  }\n}\n\n// ============================================================================\n// Provider Context & Types\n// ============================================================================\n\nexport interface AttachmentsContext {\n  files: (FileUIPart & { id: string })[]\n  add: (files: File[] | FileList) => void\n  remove: (id: string) => void\n  clear: () => void\n  openFileDialog: () => void\n  fileInputRef: RefObject<HTMLInputElement | null>\n}\n\nexport interface TextInputContext {\n  value: string\n  setInput: (v: string) => void\n  clear: () => void\n}\n\nexport interface PromptInputControllerProps {\n  textInput: TextInputContext\n  attachments: AttachmentsContext\n  /** INTERNAL: Allows PromptInput to register its file textInput + \"open\" callback */\n  __registerFileInput: (\n    ref: RefObject<HTMLInputElement | null>,\n    open: () => void\n  ) => void\n}\n\nconst PromptInputController = createContext<PromptInputControllerProps | null>(\n  null\n)\nconst ProviderAttachmentsContext = createContext<AttachmentsContext | null>(\n  null\n)\n// The provider's attachment list and text as add/remove/clear and\n// setInput/clear leave them, before React commits. PromptInput checks\n// maxFiles against the list, so an add() in the same handler as a remove()\n// or clear() sees the freed slot, and a submit takes the text from here and\n// clears it, so a second submit in the same tick does not send it again.\ntype ProviderRefs = {\n  files: RefObject<AttachmentFile[]>\n  text: RefObject<string>\n}\nconst ProviderRefsContext = createContext<ProviderRefs | null>(null)\n\nexport const usePromptInputController = () => {\n  const ctx = useContext(PromptInputController)\n  if (!ctx) {\n    throw new Error(\n      \"Wrap your component inside <PromptInputProvider> to use usePromptInputController().\"\n    )\n  }\n  return ctx\n}\n\n// Optional variants (do NOT throw). Useful for dual-mode components.\nconst useOptionalPromptInputController = () => useContext(PromptInputController)\n\nexport const useProviderAttachments = () => {\n  const ctx = useContext(ProviderAttachmentsContext)\n  if (!ctx) {\n    throw new Error(\n      \"Wrap your component inside <PromptInputProvider> to use useProviderAttachments().\"\n    )\n  }\n  return ctx\n}\n\nconst useOptionalProviderAttachments = () =>\n  useContext(ProviderAttachmentsContext)\n\nexport type PromptInputProviderProps = PropsWithChildren<{\n  initialInput?: string\n}>\n\n/**\n * Optional global provider that lifts PromptInput state outside of PromptInput.\n * If you don't use it, PromptInput stays fully self-managed.\n */\nexport const PromptInputProvider = ({\n  initialInput: initialTextInput = \"\",\n  children,\n}: PromptInputProviderProps) => {\n  // ----- textInput state\n  const [textInput, setTextInput] = useState(initialTextInput)\n  const textInputRef = useRef(initialTextInput)\n  const setInput = useCallback((value: string) => {\n    textInputRef.current = value\n    setTextInput(value)\n  }, [])\n  const clearInput = useCallback(() => setInput(\"\"), [setInput])\n\n  // ----- attachments state (global when wrapped)\n  const [attachmentFiles, setAttachmentFiles] = useState<AttachmentFile[]>([])\n  const fileInputRef = useRef<HTMLInputElement | null>(null)\n  const openRef = useRef<() => void>(() => {\n    // replaced by PromptInput via __registerFileInput\n  })\n\n  // Mirrors the attachments synchronously so object URLs can be created and\n  // revoked outside React's state updaters, which StrictMode invokes twice.\n  const attachmentsRef = useRef(attachmentFiles)\n\n  useEffect(() => {\n    attachmentsRef.current = attachmentFiles\n  }, [attachmentFiles])\n\n  const add = useCallback((files: File[] | FileList) => {\n    const next = [...files].map(toAttachment)\n    if (next.length === 0) {\n      return\n    }\n    attachmentsRef.current = [...attachmentsRef.current, ...next]\n    setAttachmentFiles((prev) => [...prev, ...next])\n  }, [])\n\n  const remove = useCallback((id: string) => {\n    const found = attachmentsRef.current.find((f) => f.id === id)\n    if (found?.url) {\n      URL.revokeObjectURL(found.url)\n    }\n    attachmentsRef.current = attachmentsRef.current.filter((f) => f.id !== id)\n    setAttachmentFiles((prev) => prev.filter((f) => f.id !== id))\n  }, [])\n\n  const clear = useCallback(() => {\n    revokeObjectUrls(attachmentsRef.current)\n    attachmentsRef.current = []\n    setAttachmentFiles([])\n  }, [])\n\n  // Cleanup blob URLs on unmount to prevent memory leaks\n  useEffect(\n    () => () => {\n      revokeObjectUrls(attachmentsRef.current)\n    },\n    []\n  )\n\n  const openFileDialog = useCallback(() => {\n    openRef.current?.()\n  }, [])\n\n  const attachments = useMemo<AttachmentsContext>(\n    () => ({\n      add,\n      clear,\n      fileInputRef,\n      files: attachmentFiles,\n      openFileDialog,\n      remove,\n    }),\n    [attachmentFiles, add, remove, clear, openFileDialog]\n  )\n\n  const __registerFileInput = useCallback(\n    (ref: RefObject<HTMLInputElement | null>, open: () => void) => {\n      fileInputRef.current = ref.current\n      openRef.current = open\n    },\n    []\n  )\n\n  const controller = useMemo<PromptInputControllerProps>(\n    () => ({\n      __registerFileInput,\n      attachments,\n      textInput: {\n        clear: clearInput,\n        setInput,\n        value: textInput,\n      },\n    }),\n    [textInput, clearInput, setInput, attachments, __registerFileInput]\n  )\n\n  const refs = useMemo<ProviderRefs>(\n    () => ({ files: attachmentsRef, text: textInputRef }),\n    []\n  )\n\n  return (\n    <PromptInputController.Provider value={controller}>\n      <ProviderAttachmentsContext.Provider value={attachments}>\n        <ProviderRefsContext.Provider value={refs}>\n          {children}\n        </ProviderRefsContext.Provider>\n      </ProviderAttachmentsContext.Provider>\n    </PromptInputController.Provider>\n  )\n}\n\n// ============================================================================\n// Component Context & Hooks\n// ============================================================================\n\nconst LocalAttachmentsContext = createContext<AttachmentsContext | null>(null)\n\nexport const usePromptInputAttachments = () => {\n  // Prefer local context (inside PromptInput) as it has validation, fall back to provider\n  const provider = useOptionalProviderAttachments()\n  const local = useContext(LocalAttachmentsContext)\n  const context = local ?? provider\n  if (!context) {\n    throw new Error(\n      \"usePromptInputAttachments must be used within a PromptInput or PromptInputProvider\"\n    )\n  }\n  return context\n}\n\nexport type PromptInputError = {\n  code: \"max_files\" | \"max_file_size\" | \"accept\" | \"screenshot\"\n  message: string\n}\n\nconst PromptInputErrorContext = createContext<\n  ((error: PromptInputError) => void) | undefined\n>(undefined)\n\n// Set by PromptInputTextarea so a rejected submit does not write into a\n// textarea the consumer controls through `value`.\nconst TextareaControlContext = createContext<RefObject<boolean> | null>(null)\n\n// ============================================================================\n// Referenced Sources (Local to PromptInput)\n// ============================================================================\n\nexport interface ReferencedSourcesContext {\n  sources: (SourceDocumentUIPart & { id: string })[]\n  add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void\n  remove: (id: string) => void\n  clear: () => void\n}\n\nexport const LocalReferencedSourcesContext =\n  createContext<ReferencedSourcesContext | null>(null)\n\nexport const usePromptInputReferencedSources = () => {\n  const ctx = useContext(LocalReferencedSourcesContext)\n  if (!ctx) {\n    throw new Error(\n      \"usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider\"\n    )\n  }\n  return ctx\n}\n\n/** Base UI menu items expose `onClick` (not Radix `onSelect`). */\ntype PromptInputActionMenuItemClickEvent = Parameters<\n  NonNullable<ComponentProps<typeof DropdownMenuItem>[\"onClick\"]>\n>[0]\n\nexport type PromptInputActionAddAttachmentsProps = ComponentProps<\n  typeof DropdownMenuItem\n> & {\n  label?: string\n}\n\nexport const PromptInputActionAddAttachments = ({\n  label = \"Add photos or files\",\n  onClick,\n  ...props\n}: PromptInputActionAddAttachmentsProps) => {\n  const attachments = usePromptInputAttachments()\n\n  const handleClick = useCallback(\n    (event: PromptInputActionMenuItemClickEvent) => {\n      onClick?.(event)\n      if (event.defaultPrevented) {\n        return\n      }\n      attachments.openFileDialog()\n    },\n    [onClick, attachments]\n  )\n\n  // Upstream keeps the menu open while the file dialog is up by calling\n  // `preventDefault()` in Radix `onSelect`; Base UI's equivalent is `closeOnClick`.\n  return (\n    <DropdownMenuItem closeOnClick={false} {...props} onClick={handleClick}>\n      <ImageIcon className=\"mr-2 size-4\" /> {label}\n    </DropdownMenuItem>\n  )\n}\n\nexport type PromptInputActionAddScreenshotProps = ComponentProps<\n  typeof DropdownMenuItem\n> & {\n  label?: string\n}\n\nexport const PromptInputActionAddScreenshot = ({\n  label = \"Take screenshot\",\n  onClick,\n  ...props\n}: PromptInputActionAddScreenshotProps) => {\n  const attachments = usePromptInputAttachments()\n  const onError = useContext(PromptInputErrorContext)\n\n  const handleClick = useCallback(\n    async (event: PromptInputActionMenuItemClickEvent) => {\n      onClick?.(event)\n      if (event.defaultPrevented) {\n        return\n      }\n\n      try {\n        const screenshot = await captureScreenshot()\n        if (screenshot) {\n          attachments.add([screenshot])\n        }\n      } catch (error) {\n        if (\n          error instanceof DOMException &&\n          (error.name === \"NotAllowedError\" || error.name === \"AbortError\")\n        ) {\n          return\n        }\n        const message =\n          error instanceof Error && error.message\n            ? error.message\n            : \"Screenshot capture failed.\"\n        if (onError) {\n          onError({ code: \"screenshot\", message })\n        } else {\n          console.error(error)\n        }\n      }\n    },\n    [onClick, attachments, onError]\n  )\n\n  return (\n    <DropdownMenuItem {...props} onClick={handleClick}>\n      <Monitor className=\"mr-2 size-4\" />\n      {label}\n    </DropdownMenuItem>\n  )\n}\n\nexport interface PromptInputMessage {\n  text: string\n  files: FileUIPart[]\n}\n\nexport type PromptInputProps = Omit<\n  HTMLAttributes<HTMLFormElement>,\n  \"onSubmit\" | \"onError\"\n> & {\n  // e.g., \"image/*\" or leave undefined for any\n  accept?: string\n  multiple?: boolean\n  // When true, accepts drops anywhere on document. Default false (opt-in).\n  globalDrop?: boolean\n  // Kept for API parity with AI Elements. Browsers do not let scripts set a\n  // file input's value, so this only clears the hidden input when the\n  // attachment list empties; native form posts never carry the attachments.\n  syncHiddenInput?: boolean\n  // Minimal constraints\n  maxFiles?: number\n  // bytes\n  maxFileSize?: number\n  onError?: (err: PromptInputError) => void\n  // Returning `false` (or throwing/rejecting) rejects the submit: the text is\n  // restored and the attachments and referenced sources are kept.\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n    // biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps `async () => {}` and useChat's sendMessage (Promise<void>) assignable alongside a `false` return.\n  ) => void | boolean | Promise<void | boolean>\n}\n\nexport const PromptInput = ({\n  className,\n  accept,\n  multiple,\n  globalDrop,\n  syncHiddenInput,\n  maxFiles,\n  maxFileSize,\n  onError,\n  onSubmit,\n  children,\n  ...props\n}: PromptInputProps) => {\n  // Try to use a provider controller if present\n  const controller = useOptionalPromptInputController()\n  const usingProvider = !!controller\n  const providerRefs = useContext(ProviderRefsContext)\n\n  // Refs\n  const inputRef = useRef<HTMLInputElement | null>(null)\n  const formRef = useRef<HTMLFormElement | null>(null)\n\n  // ----- Local attachments (only used when no provider)\n  const [items, setItems] = useState<AttachmentFile[]>([])\n  const files = usingProvider ? controller.attachments.files : items\n\n  // ----- Local referenced sources (always local to PromptInput)\n  const [referencedSources, setReferencedSources] = useState<\n    (SourceDocumentUIPart & { id: string })[]\n  >([])\n\n  // Mirrors `files` synchronously: validation, object URLs and `onError` run\n  // outside React's state updaters (StrictMode invokes those twice), and the\n  // unmount cleanup needs the latest list without a stale closure.\n  const filesRef = useRef(files)\n\n  useEffect(() => {\n    filesRef.current = files\n  }, [files])\n\n  // The attachments as of now, not as of the last render: add, remove and\n  // clear update these synchronously (the provider's own ref in provider\n  // mode), so maxFiles and a submit's snapshot see changes made earlier in\n  // the same handler.\n  const currentFiles = useCallback(\n    () =>\n      (usingProvider ? providerRefs?.files.current : undefined) ??\n      filesRef.current,\n    [usingProvider, providerRefs]\n  )\n\n  // Ids of attachments carried by a submit that has not settled yet: a\n  // second submit (a double press, or one while an async onSubmit is\n  // pending) leaves them out instead of sending them again.\n  const inFlightFileIdsRef = useRef(new Set<string>())\n  // Settles once the latest submit has called onSubmit, so submits reach\n  // onSubmit in the order they were made even when an earlier one is still\n  // converting its attachments.\n  const submitTurnRef = useRef<Promise<void>>(Promise.resolve())\n\n  const textareaControlledRef = useRef(false)\n\n  const openFileDialogLocal = useCallback(() => {\n    inputRef.current?.click()\n  }, [])\n\n  const matchesAccept = useCallback(\n    (f: File) => {\n      if (!accept || accept.trim() === \"\") {\n        return true\n      }\n\n      const patterns = accept\n        .split(\",\")\n        .map((s) => s.trim().toLowerCase())\n        .filter(Boolean)\n      const type = f.type.toLowerCase()\n      const name = f.name.toLowerCase()\n\n      return patterns.some((pattern) => {\n        if (pattern === \"*/*\") {\n          return true\n        }\n        if (pattern.startsWith(\".\")) {\n          // e.g: .pdf -> matches by file name, which also covers files the\n          // OS reports with an empty type\n          return name.endsWith(pattern)\n        }\n        if (pattern.endsWith(\"/*\")) {\n          // e.g: image/* -> image/\n          const prefix = pattern.slice(0, -1)\n          return type.startsWith(prefix)\n        }\n        return type === pattern\n      })\n    },\n    [accept]\n  )\n\n  // Applies accept, maxFileSize and maxFiles in that order and reports every\n  // file that was dropped, so a partially rejected batch is never silent.\n  const acceptFiles = useCallback(\n    (fileList: File[] | FileList, currentCount: number): File[] => {\n      const incoming = [...fileList]\n      if (incoming.length === 0) {\n        return []\n      }\n      const accepted = incoming.filter((f) => matchesAccept(f))\n      if (accepted.length === 0) {\n        onError?.({\n          code: \"accept\",\n          message: \"No files match the accepted types.\",\n        })\n        return []\n      }\n      if (accepted.length < incoming.length) {\n        onError?.({\n          code: \"accept\",\n          message:\n            \"Some files do not match the accepted types and were not added.\",\n        })\n      }\n      const sized = maxFileSize\n        ? accepted.filter((f) => f.size <= maxFileSize)\n        : accepted\n      if (sized.length === 0) {\n        onError?.({\n          code: \"max_file_size\",\n          message: \"All files exceed the maximum size.\",\n        })\n        return []\n      }\n      if (sized.length < accepted.length) {\n        onError?.({\n          code: \"max_file_size\",\n          message: \"Some files exceed the maximum size and were not added.\",\n        })\n      }\n      if (typeof maxFiles !== \"number\") {\n        return sized\n      }\n      const capacity = Math.max(0, maxFiles - currentCount)\n      if (sized.length > capacity) {\n        onError?.({\n          code: \"max_files\",\n          message: \"Too many files. Some were not added.\",\n        })\n      }\n      return sized.slice(0, capacity)\n    },\n    [matchesAccept, maxFileSize, maxFiles, onError]\n  )\n\n  const addLocal = useCallback(\n    (fileList: File[] | FileList) => {\n      const next = acceptFiles(fileList, filesRef.current.length).map(\n        toAttachment\n      )\n      if (next.length === 0) {\n        return\n      }\n      filesRef.current = [...filesRef.current, ...next]\n      setItems((prev) => [...prev, ...next])\n    },\n    [acceptFiles]\n  )\n\n  const removeLocal = useCallback((id: string) => {\n    const found = filesRef.current.find((file) => file.id === id)\n    if (found?.url) {\n      URL.revokeObjectURL(found.url)\n    }\n    filesRef.current = filesRef.current.filter((file) => file.id !== id)\n    setItems((prev) => prev.filter((file) => file.id !== id))\n  }, [])\n\n  // Wrapper that validates files before calling provider's add\n  const addWithProviderValidation = useCallback(\n    (fileList: File[] | FileList) => {\n      const capped = acceptFiles(fileList, currentFiles().length)\n      if (capped.length > 0) {\n        controller?.attachments.add(capped)\n      }\n    },\n    [acceptFiles, controller, currentFiles]\n  )\n\n  const clearAttachments = useCallback(() => {\n    if (usingProvider) {\n      controller?.attachments.clear()\n      return\n    }\n    revokeObjectUrls(filesRef.current)\n    filesRef.current = []\n    setItems([])\n  }, [usingProvider, controller])\n\n  const removeAttachments = useCallback(\n    (ids: ReadonlySet<string>) => {\n      if (usingProvider) {\n        for (const id of ids) {\n          controller?.attachments.remove(id)\n        }\n        return\n      }\n      revokeObjectUrls(filesRef.current.filter((file) => ids.has(file.id)))\n      filesRef.current = filesRef.current.filter((file) => !ids.has(file.id))\n      setItems((prev) => prev.filter((file) => !ids.has(file.id)))\n    },\n    [usingProvider, controller]\n  )\n\n  const clearReferencedSources = useCallback(() => setReferencedSources([]), [])\n\n  const removeReferencedSources = useCallback(\n    (ids: ReadonlySet<string>) =>\n      setReferencedSources((prev) =>\n        prev.filter((source) => !ids.has(source.id))\n      ),\n    []\n  )\n\n  const add = usingProvider ? addWithProviderValidation : addLocal\n  const remove = usingProvider ? controller.attachments.remove : removeLocal\n  const openFileDialog = usingProvider\n    ? controller.attachments.openFileDialog\n    : openFileDialogLocal\n\n  // Let provider know about our hidden file input so external menus can call openFileDialog()\n  useEffect(() => {\n    if (!usingProvider) {\n      return\n    }\n    controller.__registerFileInput(inputRef, () => inputRef.current?.click())\n  }, [usingProvider, controller])\n\n  useEffect(() => {\n    if (syncHiddenInput && inputRef.current && files.length === 0) {\n      inputRef.current.value = \"\"\n    }\n  }, [files, syncHiddenInput])\n\n  // Attach drop handlers on nearest form and document (opt-in)\n  useEffect(() => {\n    const form = formRef.current\n    if (!form) {\n      return\n    }\n    if (globalDrop) {\n      // when global drop is on, let the document-level handler own drops\n      return\n    }\n\n    const onDragOver = (e: DragEvent) => {\n      if (e.dataTransfer?.types?.includes(\"Files\")) {\n        e.preventDefault()\n      }\n    }\n    const onDrop = (e: DragEvent) => {\n      if (e.dataTransfer?.types?.includes(\"Files\")) {\n        e.preventDefault()\n      }\n      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n        add(e.dataTransfer.files)\n      }\n    }\n    form.addEventListener(\"dragover\", onDragOver)\n    form.addEventListener(\"drop\", onDrop)\n    return () => {\n      form.removeEventListener(\"dragover\", onDragOver)\n      form.removeEventListener(\"drop\", onDrop)\n    }\n  }, [add, globalDrop])\n\n  useEffect(() => {\n    if (!globalDrop) {\n      return\n    }\n\n    const onDragOver = (e: DragEvent) => {\n      if (e.dataTransfer?.types?.includes(\"Files\")) {\n        e.preventDefault()\n      }\n    }\n    const onDrop = (e: DragEvent) => {\n      if (e.dataTransfer?.types?.includes(\"Files\")) {\n        e.preventDefault()\n      }\n      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n        add(e.dataTransfer.files)\n      }\n    }\n    document.addEventListener(\"dragover\", onDragOver)\n    document.addEventListener(\"drop\", onDrop)\n    return () => {\n      document.removeEventListener(\"dragover\", onDragOver)\n      document.removeEventListener(\"drop\", onDrop)\n    }\n  }, [add, globalDrop])\n\n  useEffect(\n    () => () => {\n      if (!usingProvider) {\n        revokeObjectUrls(filesRef.current)\n      }\n    },\n    [usingProvider]\n  )\n\n  const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(\n    (event) => {\n      if (event.currentTarget.files) {\n        add(event.currentTarget.files)\n      }\n      // Reset input value to allow selecting files that were previously removed\n      event.currentTarget.value = \"\"\n    },\n    [add]\n  )\n\n  const attachmentsCtx = useMemo<AttachmentsContext>(\n    () => ({\n      add,\n      clear: clearAttachments,\n      fileInputRef: inputRef,\n      files: files.map((item) => ({ ...item, id: item.id })),\n      openFileDialog,\n      remove,\n    }),\n    [files, add, remove, clearAttachments, openFileDialog]\n  )\n\n  const refsCtx = useMemo<ReferencedSourcesContext>(\n    () => ({\n      add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {\n        const array = Array.isArray(incoming) ? incoming : [incoming]\n        setReferencedSources((prev) => [\n          ...prev,\n          ...array.map((s) => ({ ...s, id: nanoid() })),\n        ])\n      },\n      clear: clearReferencedSources,\n      remove: (id: string) => {\n        setReferencedSources((prev) => prev.filter((s) => s.id !== id))\n      },\n      sources: referencedSources,\n    }),\n    [referencedSources, clearReferencedSources]\n  )\n\n  const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(\n    async (event) => {\n      event.preventDefault()\n\n      const form = event.currentTarget\n      const text = usingProvider\n        ? (providerRefs?.text.current ?? controller.textInput.value)\n        : (() => {\n            const formData = new FormData(form)\n            return (formData.get(\"message\") as string) || \"\"\n          })()\n\n      // Clear the text as soon as it is captured, in both modes, so input\n      // typed during the async blob conversion or a pending onSubmit is kept,\n      // and a second submit in the meantime does not send the same text.\n      if (usingProvider) {\n        controller.textInput.clear()\n      } else {\n        form.reset()\n      }\n\n      // Only what this submit carried is cleared once it is accepted, so a\n      // file attached while a slow onSubmit is still pending survives.\n      const inFlight = inFlightFileIdsRef.current\n      const submittedFiles = currentFiles().filter(\n        (file) => !inFlight.has(file.id)\n      )\n      const submittedFileIds = new Set(submittedFiles.map((file) => file.id))\n      for (const id of submittedFileIds) {\n        inFlight.add(id)\n      }\n      const submittedSourceIds = new Set(\n        referencedSources.map((source) => source.id)\n      )\n\n      const previousTurn = submitTurnRef.current\n      let endTurn = () => {}\n      submitTurnRef.current = new Promise<void>((resolve) => {\n        endTurn = resolve\n      })\n\n      const commit = () => {\n        removeAttachments(submittedFileIds)\n        removeReferencedSources(submittedSourceIds)\n      }\n\n      // A rejected submit gives the text back so the user can retry, unless\n      // they have typed something new since the reset. A controlled textarea\n      // belongs to the consumer, so it is left alone.\n      const restoreText = () => {\n        if (usingProvider) {\n          if (providerRefs?.text.current === \"\") {\n            controller.textInput.setInput(text)\n          }\n          return\n        }\n        if (textareaControlledRef.current) {\n          return\n        }\n        const textarea = form.querySelector<HTMLTextAreaElement>(\n          'textarea[name=\"message\"]'\n        )\n        if (textarea && textarea.value === textarea.defaultValue) {\n          textarea.value = text\n        }\n      }\n\n      try {\n        // Convert blob URLs to data URLs asynchronously\n        const convertedFiles: FileUIPart[] = await Promise.all(\n          submittedFiles.map(async ({ id: _id, ...item }) => {\n            if (item.url?.startsWith(\"blob:\")) {\n              const dataUrl = await convertBlobUrlToDataUrl(item.url)\n              // If conversion failed, keep the original blob URL\n              return {\n                ...item,\n                url: dataUrl ?? item.url,\n              }\n            }\n            return item\n          })\n        )\n\n        await previousTurn\n        const result = onSubmit({ files: convertedFiles, text }, event)\n        endTurn()\n        // A sync onSubmit commits in the same tick; an async one once it\n        // settles. Throwing, rejecting or returning false rejects the submit.\n        const accepted = result instanceof Promise ? await result : result\n        if (accepted === false) {\n          restoreText()\n        } else {\n          commit()\n        }\n      } catch {\n        restoreText()\n      } finally {\n        endTurn()\n        for (const id of submittedFileIds) {\n          inFlight.delete(id)\n        }\n      }\n    },\n    [\n      usingProvider,\n      controller,\n      providerRefs,\n      currentFiles,\n      referencedSources,\n      onSubmit,\n      removeAttachments,\n      removeReferencedSources,\n    ]\n  )\n\n  // Render with or without local provider\n  const inner = (\n    <>\n      <input\n        accept={accept}\n        aria-label=\"Upload files\"\n        className=\"hidden\"\n        multiple={multiple}\n        onChange={handleChange}\n        ref={inputRef}\n        title=\"Upload files\"\n        type=\"file\"\n      />\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        ref={formRef}\n        {...props}\n      >\n        <InputGroup className=\"overflow-hidden\">{children}</InputGroup>\n      </form>\n    </>\n  )\n\n  const withReferencedSources = (\n    <LocalReferencedSourcesContext.Provider value={refsCtx}>\n      <TextareaControlContext.Provider value={textareaControlledRef}>\n        {inner}\n      </TextareaControlContext.Provider>\n    </LocalReferencedSourcesContext.Provider>\n  )\n\n  // Always provide LocalAttachmentsContext so children get validated add function\n  return (\n    <LocalAttachmentsContext.Provider value={attachmentsCtx}>\n      <PromptInputErrorContext.Provider value={onError}>\n        {withReferencedSources}\n      </PromptInputErrorContext.Provider>\n    </LocalAttachmentsContext.Provider>\n  )\n}\n\nexport type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputBody = ({\n  className,\n  ...props\n}: PromptInputBodyProps) => (\n  <div className={cn(\"contents\", className)} {...props} />\n)\n\nexport type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>\n\nexport const PromptInputTextarea = ({\n  onChange,\n  onKeyDown,\n  className,\n  placeholder = \"What would you like to know?\",\n  ...props\n}: PromptInputTextareaProps) => {\n  const controller = useOptionalPromptInputController()\n  const attachments = usePromptInputAttachments()\n  const [isComposing, setIsComposing] = useState(false)\n\n  const controlledRef = useContext(TextareaControlContext)\n  const isControlled = !controller && props.value !== undefined\n  useEffect(() => {\n    if (controlledRef) {\n      controlledRef.current = isControlled\n    }\n  }, [controlledRef, isControlled])\n\n  const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(\n    (e) => {\n      // Call the external onKeyDown handler first\n      onKeyDown?.(e)\n\n      // If the external handler prevented default, don't run internal logic\n      if (e.defaultPrevented) {\n        return\n      }\n\n      if (e.key === \"Enter\") {\n        // keyCode 229 marks a key handled by an IME; Safari fires it on the\n        // Enter that confirms a composition after `compositionend` has run.\n        if (\n          isComposing ||\n          e.nativeEvent.isComposing ||\n          e.nativeEvent.keyCode === 229\n        ) {\n          return\n        }\n        if (e.shiftKey) {\n          return\n        }\n        e.preventDefault()\n\n        // Enter only submits through an enabled submit button, so a disabled\n        // button or a Stop button (type=\"button\" while generating) blocks it\n        const { form } = e.currentTarget\n        if (!form) {\n          return\n        }\n        const submitButton = form.querySelector<HTMLButtonElement>(\n          'button[type=\"submit\"]'\n        )\n        if (!submitButton || submitButton.disabled) {\n          return\n        }\n\n        form.requestSubmit()\n      }\n\n      // Remove last attachment when Backspace is pressed and textarea is empty\n      if (\n        e.key === \"Backspace\" &&\n        e.currentTarget.value === \"\" &&\n        attachments.files.length > 0\n      ) {\n        e.preventDefault()\n        const lastAttachment = attachments.files.at(-1)\n        if (lastAttachment) {\n          attachments.remove(lastAttachment.id)\n        }\n      }\n    },\n    [onKeyDown, isComposing, attachments]\n  )\n\n  const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = useCallback(\n    (event) => {\n      const items = event.clipboardData?.items\n\n      if (!items) {\n        return\n      }\n\n      const files: File[] = []\n\n      for (const item of items) {\n        if (item.kind === \"file\") {\n          const file = item.getAsFile()\n          if (file) {\n            files.push(file)\n          }\n        }\n      }\n\n      if (files.length > 0) {\n        event.preventDefault()\n        attachments.add(files)\n      }\n    },\n    [attachments]\n  )\n\n  const handleCompositionEnd = useCallback(() => setIsComposing(false), [])\n  const handleCompositionStart = useCallback(() => setIsComposing(true), [])\n\n  const controlledProps = controller\n    ? {\n        onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n          controller.textInput.setInput(e.currentTarget.value)\n          onChange?.(e)\n        },\n        value: controller.textInput.value,\n      }\n    : {\n        onChange,\n      }\n\n  return (\n    <InputGroupTextarea\n      aria-label=\"Message\"\n      className={cn(\"field-sizing-content max-h-48 min-h-16\", className)}\n      name=\"message\"\n      onCompositionEnd={handleCompositionEnd}\n      onCompositionStart={handleCompositionStart}\n      onKeyDown={handleKeyDown}\n      onPaste={handlePaste}\n      placeholder={placeholder}\n      {...props}\n      {...controlledProps}\n    />\n  )\n}\n\n// Anything that takes focus or handles clicks itself; a click on one of these\n// inside an addon must not move focus to the textarea.\nconst INTERACTIVE_SELECTOR = [\n  \"button\",\n  \"a[href]\",\n  \"input\",\n  \"select\",\n  \"textarea\",\n  \"summary\",\n  \"label\",\n  '[contenteditable]:not([contenteditable=\"false\"])',\n  '[tabindex]:not([tabindex=\"-1\"])',\n  '[role=\"button\"]',\n  '[role=\"link\"]',\n  '[role=\"switch\"]',\n  '[role=\"checkbox\"]',\n  '[role=\"radio\"]',\n  '[role=\"slider\"]',\n  '[role=\"tab\"]',\n  '[role=\"menuitem\"]',\n  '[role=\"option\"]',\n  '[role=\"combobox\"]',\n  '[role=\"textbox\"]',\n].join(\", \")\n\n// shadcn's InputGroupAddon focuses the group's first <input> when its surface\n// is clicked. In a composer that input is Base UI Select's hidden input, which\n// forwards focus to the select trigger, and clicks on portaled menu items\n// reach the addon through React's tree. Focus the textarea instead, and only\n// for clicks that land on the addon's own inert surface.\nconst useAddonClick = (\n  onClick: MouseEventHandler<HTMLDivElement> | undefined\n) =>\n  useCallback(\n    (event: MouseEvent<HTMLDivElement>) => {\n      onClick?.(event)\n      if (event.defaultPrevented) {\n        return\n      }\n      const { currentTarget, target } = event\n      if (!(target instanceof Element) || !currentTarget.contains(target)) {\n        return\n      }\n      if (target.closest(INTERACTIVE_SELECTOR)) {\n        return\n      }\n      const group =\n        currentTarget.closest('[data-slot=\"input-group\"]') ??\n        currentTarget.parentElement\n      group?.querySelector(\"textarea\")?.focus()\n    },\n    [onClick]\n  )\n\nexport type PromptInputHeaderProps = Omit<\n  ComponentProps<typeof InputGroupAddon>,\n  \"align\"\n>\n\nexport const PromptInputHeader = ({\n  className,\n  onClick,\n  ...props\n}: PromptInputHeaderProps) => {\n  const handleClick = useAddonClick(onClick)\n  return (\n    <InputGroupAddon\n      align=\"block-end\"\n      className={cn(\"order-first flex-wrap gap-1\", className)}\n      onClick={handleClick}\n      {...props}\n    />\n  )\n}\n\nexport type PromptInputFooterProps = Omit<\n  ComponentProps<typeof InputGroupAddon>,\n  \"align\"\n>\n\nexport const PromptInputFooter = ({\n  className,\n  onClick,\n  ...props\n}: PromptInputFooterProps) => {\n  const handleClick = useAddonClick(onClick)\n  return (\n    <InputGroupAddon\n      align=\"block-end\"\n      className={cn(\"justify-between gap-1\", className)}\n      onClick={handleClick}\n      {...props}\n    />\n  )\n}\n\nexport type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTools = ({\n  className,\n  ...props\n}: PromptInputToolsProps) => (\n  <div\n    className={cn(\"flex min-w-0 flex-wrap items-center gap-1\", className)}\n    {...props}\n  />\n)\n\nexport type PromptInputButtonTooltip =\n  | string\n  | {\n      content: ReactNode\n      shortcut?: string\n      side?: ComponentProps<typeof TooltipContent>[\"side\"]\n    }\n\nexport type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {\n  tooltip?: PromptInputButtonTooltip\n}\n\n/** The text a node renders, read from React children (icons contribute nothing). */\nconst textOf = (node: ReactNode): string => {\n  if (typeof node === \"string\" || typeof node === \"number\") {\n    return String(node)\n  }\n  if (Array.isArray(node)) {\n    return node.map(textOf).join(\"\")\n  }\n  if (isValidElement<{ children?: ReactNode }>(node)) {\n    return textOf(node.props.children)\n  }\n  return \"\"\n}\n\n// Key names for aria-keyshortcuts, which wants UI Events key values joined by\n// \"+\" (\"Meta+K\"), not the glyphs a tooltip shows (\"⌘K\").\nconst KEY_GLYPHS: Record<string, string> = {\n  \"⌘\": \"Meta\",\n  \"⌃\": \"Control\",\n  \"⌥\": \"Alt\",\n  \"⇧\": \"Shift\",\n  \"↵\": \"Enter\",\n  \"⏎\": \"Enter\",\n  \"⌫\": \"Backspace\",\n  \"⌦\": \"Delete\",\n  \"⎋\": \"Escape\",\n  \"⇥\": \"Tab\",\n  \"↑\": \"ArrowUp\",\n  \"↓\": \"ArrowDown\",\n  \"←\": \"ArrowLeft\",\n  \"→\": \"ArrowRight\",\n}\n\nconst KEY_WORDS: Record<string, string> = {\n  alt: \"Alt\",\n  cmd: \"Meta\",\n  command: \"Meta\",\n  control: \"Control\",\n  ctrl: \"Control\",\n  del: \"Delete\",\n  enter: \"Enter\",\n  esc: \"Escape\",\n  escape: \"Escape\",\n  meta: \"Meta\",\n  opt: \"Alt\",\n  option: \"Alt\",\n  return: \"Enter\",\n  shift: \"Shift\",\n  space: \"Space\",\n  tab: \"Tab\",\n}\n\nconst toKeyShortcuts = (shortcut: string): string | undefined => {\n  const keys: string[] = []\n  for (const token of shortcut.split(/[\\s+]+/)) {\n    let rest = token\n    // Glyphs run together (\"⌘⇧K\"), so peel them off one at a time.\n    while (rest !== \"\") {\n      const glyph = KEY_GLYPHS[rest.charAt(0)]\n      if (!glyph) break\n      keys.push(glyph)\n      rest = rest.slice(1)\n    }\n    if (rest === \"\") continue\n    const word = KEY_WORDS[rest.toLowerCase()]\n    keys.push(word ?? (rest.length === 1 ? rest.toUpperCase() : rest))\n  }\n  return keys.length > 0 ? keys.join(\"+\") : undefined\n}\n\nexport const PromptInputButton = ({\n  variant = \"ghost\",\n  className,\n  size,\n  tooltip,\n  ...props\n}: PromptInputButtonProps) => {\n  const descriptionId = useId()\n  const newSize =\n    size ?? (Children.count(props.children) > 1 ? \"sm\" : \"icon-sm\")\n\n  const tooltipContent =\n    typeof tooltip === \"string\" ? tooltip : tooltip?.content\n  const hasTooltip = Boolean(tooltipContent)\n  const shortcut = typeof tooltip === \"string\" ? undefined : tooltip?.shortcut\n  const side = typeof tooltip === \"string\" ? \"top\" : (tooltip?.side ?? \"top\")\n  // Base UI tooltips are visual only (no role or aria-describedby), so the\n  // content is mirrored in a visually hidden description unless it would\n  // just repeat the button's own name (its aria-label or visible text).\n  const repeatsName =\n    typeof tooltipContent === \"string\" &&\n    [props[\"aria-label\"], textOf(props.children)].some(\n      (name) => name?.trim() === tooltipContent.trim()\n    )\n  const describes = hasTooltip && !repeatsName\n  // The shortcut survives a skipped description as aria-keyshortcuts.\n  const keyShortcuts =\n    hasTooltip && shortcut ? toKeyShortcuts(shortcut) : undefined\n\n  const button = (\n    <InputGroupButton\n      aria-describedby={describes ? descriptionId : undefined}\n      aria-keyshortcuts={keyShortcuts}\n      className={cn(className)}\n      size={newSize}\n      type=\"button\"\n      variant={variant}\n      {...props}\n    />\n  )\n\n  if (!hasTooltip) {\n    return button\n  }\n\n  return (\n    <>\n      <Tooltip>\n        <TooltipTrigger delay={0} render={button} />\n        <TooltipContent side={side}>\n          {tooltipContent}\n          {shortcut && (\n            <span className=\"ml-2 text-muted-foreground\">{shortcut}</span>\n          )}\n        </TooltipContent>\n      </Tooltip>\n      {describes && (\n        <span className=\"sr-only\" id={descriptionId}>\n          {tooltipContent}\n          {shortcut && ` ${shortcut}`}\n        </span>\n      )}\n    </>\n  )\n}\n\nexport type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>\nexport const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (\n  <DropdownMenu {...props} />\n)\n\nexport type PromptInputActionMenuTriggerProps = PromptInputButtonProps\n\nexport const PromptInputActionMenuTrigger = ({\n  className,\n  children,\n  ...props\n}: PromptInputActionMenuTriggerProps) => (\n  <DropdownMenuTrigger\n    render={\n      <PromptInputButton className={className} {...props}>\n        {children ?? <PlusIcon className=\"size-4\" />}\n      </PromptInputButton>\n    }\n  />\n)\n\nexport type PromptInputActionMenuContentProps = ComponentProps<\n  typeof DropdownMenuContent\n>\nexport const PromptInputActionMenuContent = ({\n  className,\n  ...props\n}: PromptInputActionMenuContentProps) => (\n  <DropdownMenuContent align=\"start\" className={cn(className)} {...props} />\n)\n\nexport type PromptInputActionMenuItemProps = ComponentProps<\n  typeof DropdownMenuItem\n>\nexport const PromptInputActionMenuItem = ({\n  className,\n  ...props\n}: PromptInputActionMenuItemProps) => (\n  <DropdownMenuItem className={cn(className)} {...props} />\n)\n\n// Note: Actions that perform side-effects (like opening a file dialog)\n// are provided in opt-in modules (e.g., prompt-input-attachments).\n\nexport type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {\n  status?: ChatStatus\n  onStop?: () => void\n}\n\ntype PromptInputSubmitClickEvent = Parameters<\n  NonNullable<PromptInputSubmitProps[\"onClick\"]>\n>[0]\n\nexport const PromptInputSubmit = ({\n  className,\n  variant = \"default\",\n  size = \"icon-sm\",\n  status,\n  onStop,\n  onClick,\n  children,\n  ...props\n}: PromptInputSubmitProps) => {\n  const isGenerating = status === \"submitted\" || status === \"streaming\"\n  // Only a wired onStop makes this a Stop button. Without one a press still\n  // submits, so the name, the glyph and the type stay those of Submit.\n  const canStop = isGenerating && onStop !== undefined\n\n  let Icon = <CornerDownLeftIcon className=\"size-4\" />\n\n  if (status === \"submitted\") {\n    // Progress rather than an action, so it shows with or without onStop.\n    Icon = <Spinner />\n  } else if (status === \"streaming\" && canStop) {\n    Icon = <SquareIcon className=\"size-4\" />\n  } else if (status === \"error\") {\n    Icon = <XIcon className=\"size-4\" />\n  }\n\n  const handleClick = useCallback(\n    (e: PromptInputSubmitClickEvent) => {\n      if (canStop) {\n        e.preventDefault()\n        onStop?.()\n        return\n      }\n      onClick?.(e)\n    },\n    [canStop, onStop, onClick]\n  )\n\n  return (\n    <InputGroupButton\n      aria-label={canStop ? \"Stop\" : \"Submit\"}\n      className={cn(\"shrink-0\", className)}\n      onClick={handleClick}\n      size={size}\n      type={canStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {children ?? Icon}\n    </InputGroupButton>\n  )\n}\n\nexport type PromptInputSelectProps = ComponentProps<typeof Select>\n\n// Annotated with `typeof Select` so Base UI's generic value type survives the\n// wrapper (otherwise `onValueChange` receives `unknown`).\nexport const PromptInputSelect: typeof Select = (props) => <Select {...props} />\n\nexport type PromptInputSelectTriggerProps = ComponentProps<typeof SelectTrigger>\n\nexport const PromptInputSelectTrigger = ({\n  className,\n  ...props\n}: PromptInputSelectTriggerProps) => (\n  <SelectTrigger\n    className={cn(\n      \"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors\",\n      \"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground\",\n      className\n    )}\n    {...props}\n  />\n)\n\nexport type PromptInputSelectContentProps = ComponentProps<typeof SelectContent>\n\nexport const PromptInputSelectContent = ({\n  className,\n  ...props\n}: PromptInputSelectContentProps) => (\n  <SelectContent className={cn(className)} {...props} />\n)\n\nexport type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>\n\nexport const PromptInputSelectItem = ({\n  className,\n  ...props\n}: PromptInputSelectItemProps) => (\n  <SelectItem className={cn(className)} {...props} />\n)\n\nexport type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>\n\nexport const PromptInputSelectValue = ({\n  className,\n  ...props\n}: PromptInputSelectValueProps) => (\n  <SelectValue className={cn(className)} {...props} />\n)\n\n// Base UI's PreviewCard puts open/close delays on the Trigger, not the Root.\n// Upstream exposes them on the root, so carry them to the trigger via context\n// to keep the public API identical.\nconst PromptInputHoverCardDelayContext = createContext<{\n  openDelay: number\n  closeDelay: number\n}>({ closeDelay: 0, openDelay: 0 })\n\nexport type PromptInputHoverCardProps = ComponentProps<typeof HoverCard> & {\n  openDelay?: number\n  closeDelay?: number\n}\n\nexport const PromptInputHoverCard = ({\n  openDelay = 0,\n  closeDelay = 0,\n  ...props\n}: PromptInputHoverCardProps) => {\n  const delays = useMemo(\n    () => ({ closeDelay, openDelay }),\n    [closeDelay, openDelay]\n  )\n  return (\n    <PromptInputHoverCardDelayContext.Provider value={delays}>\n      <HoverCard {...props} />\n    </PromptInputHoverCardDelayContext.Provider>\n  )\n}\n\nexport type PromptInputHoverCardTriggerProps = ComponentProps<\n  typeof HoverCardTrigger\n>\n\nexport const PromptInputHoverCardTrigger = (\n  props: PromptInputHoverCardTriggerProps\n) => {\n  const { openDelay, closeDelay } = useContext(PromptInputHoverCardDelayContext)\n  return (\n    <HoverCardTrigger closeDelay={closeDelay} delay={openDelay} {...props} />\n  )\n}\n\nexport type PromptInputHoverCardContentProps = ComponentProps<\n  typeof HoverCardContent\n>\n\nexport const PromptInputHoverCardContent = ({\n  align = \"start\",\n  ...props\n}: PromptInputHoverCardContentProps) => (\n  <HoverCardContent align={align} {...props} />\n)\n\nexport type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTabsList = ({\n  className,\n  ...props\n}: PromptInputTabsListProps) => <div className={cn(className)} {...props} />\n\nexport type PromptInputTabProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTab = ({\n  className,\n  ...props\n}: PromptInputTabProps) => <div className={cn(className)} {...props} />\n\nexport type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>\n\nexport const PromptInputTabLabel = ({\n  className,\n  ...props\n}: PromptInputTabLabelProps) => (\n  // Content provided via children in props\n  <h3\n    className={cn(\n      \"mb-2 px-3 text-xs font-medium text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n)\n\nexport type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTabBody = ({\n  className,\n  ...props\n}: PromptInputTabBodyProps) => (\n  <div className={cn(\"space-y-1\", className)} {...props} />\n)\n\nexport type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTabItem = ({\n  className,\n  ...props\n}: PromptInputTabItemProps) => (\n  <div\n    className={cn(\n      \"flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent\",\n      className\n    )}\n    {...props}\n  />\n)\n\nexport type PromptInputCommandProps = ComponentProps<typeof Command>\n\nexport const PromptInputCommand = ({\n  className,\n  ...props\n}: PromptInputCommandProps) => <Command className={cn(className)} {...props} />\n\nexport type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>\n\nexport const PromptInputCommandInput = ({\n  className,\n  ...props\n}: PromptInputCommandInputProps) => (\n  <CommandInput className={cn(className)} {...props} />\n)\n\nexport type PromptInputCommandListProps = ComponentProps<typeof CommandList>\n\nexport const PromptInputCommandList = ({\n  className,\n  ...props\n}: PromptInputCommandListProps) => (\n  <CommandList className={cn(className)} {...props} />\n)\n\nexport type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>\n\nexport const PromptInputCommandEmpty = ({\n  className,\n  ...props\n}: PromptInputCommandEmptyProps) => (\n  <CommandEmpty className={cn(className)} {...props} />\n)\n\nexport type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>\n\nexport const PromptInputCommandGroup = ({\n  className,\n  ...props\n}: PromptInputCommandGroupProps) => (\n  <CommandGroup className={cn(className)} {...props} />\n)\n\nexport type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>\n\nexport const PromptInputCommandItem = ({\n  className,\n  ...props\n}: PromptInputCommandItemProps) => (\n  <CommandItem className={cn(className)} {...props} />\n)\n\nexport type PromptInputCommandSeparatorProps = ComponentProps<\n  typeof CommandSeparator\n>\n\nexport const PromptInputCommandSeparator = ({\n  className,\n  ...props\n}: PromptInputCommandSeparatorProps) => (\n  <CommandSeparator className={cn(className)} {...props} />\n)\n",
      "type": "registry:component",
      "target": "components/ai/prompt-input.tsx"
    }
  ],
  "docs": "Derived from Vercel AI Elements (Apache-2.0, Copyright 2023 Vercel, Inc.); ported to Base UI and maintained by uifiles. Built for the Base UI styles (base-nova): it composes the Base UI wrappers from components/ui and relies on their render prop and event signatures, so it does not work on Radix or React Aria styles. Ported from Radix to Base UI. API changes vs AI Elements: (1) PromptInputActionAddScreenshot takes `onClick` instead of Radix `onSelect`; calling `event.preventDefault()` in it skips the capture, but the menu still closes (pass `closeOnClick={false}` to keep it open). Capture failures other than a denied or canceled picker go to PromptInput's `onError` as `{ code: \"screenshot\", message }`, or to `console.error` when there is no handler. (2) PromptInputActionAddAttachments and PromptInputActionMenuItem are Base UI Menu items: use `onClick` (not `onSelect`) and `closeOnClick={false}` to keep the menu open. PromptInputActionAddAttachments calls your `onClick` first, skips the file dialog if it called `preventDefault()`, and sets `closeOnClick={false}` by default to mirror upstream's preventDefault; override it with `closeOnClick`. (3) PromptInputHoverCard still accepts `openDelay`/`closeDelay` (default 0) but they are applied to PromptInputHoverCardTrigger via context because Base UI PreviewCard puts delays on the trigger; passing `delay`/`closeDelay` directly on the trigger overrides them. (4) PromptInputSelect is Base UI Select: `onValueChange` receives `(value, eventDetails)` and the value may be `null`; PromptInputSelectValue renders the raw value unless you pass `items={[{ label, value }]}` on PromptInputSelect (or a children function). (5) `PromptInputButton`/`PromptInputSubmit` `onClick` receive Base UI's event type (a superset of React.MouseEvent). Behavior fixed on top of upstream: (6) PromptInputSubmit is a Stop button (named \"Stop\", `type=\"button\"`, a press calls `onStop`, the square glyph while streaming) only while `status` is submitted/streaming and `onStop` is passed; without `onStop` a press still submits, so it keeps the Submit name, type and glyph, and only the spinner shows while submitted (upstream names it Stop and shows the square either way). Enter submits only through an enabled `button[type=\"submit\"]` inside the form, so while PromptInputSubmit shows Stop Enter does nothing and the draft is kept; a disabled or missing submit button blocks Enter the same way. (7) `accept` understands `.ext` patterns (matched case-insensitively against the file name, so files the OS reports with an empty type still match), `type/*` and `*/*`; when only some files of a batch pass `accept` or `maxFileSize`, the rest are dropped and reported through `onError`, like `max_files`; `maxFiles` is checked against the attachment list as `add()`, `remove()` and `clear()` leave it, before React re-renders, in local and in provider mode: several `add()` calls in one handler cannot exceed it, and a `remove()` or `clear()` followed by `add()` in the same handler (swapping a file) gets the freed slot, also when the removal goes through `useProviderAttachments()`. (8) `onError` codes are `\"max_files\" | \"max_file_size\" | \"accept\" | \"screenshot\"`; the `PromptInputError` type is exported. (9) `onSubmit` may return `false` (directly or from its promise) to reject a submit. A submit clears the text as it starts, also text held by PromptInputProvider (upstream clears the provider's text only once `onSubmit` succeeds, so a second submit in the meantime sent it again and text typed meanwhile was wiped). When it throws, rejects or returns `false`, an uncontrolled PromptInputTextarea or the provider gets its text back unless you typed since, and attachments and referenced sources are kept (as upstream); a textarea you control through `value` is left to you. Once a submit is accepted, only the attachments and referenced sources it carried are cleared, so a file attached while an async `onSubmit` is still pending stays. A submit leaves out attachments an earlier submit is still carrying (converting blob URLs, or waiting on an async `onSubmit`), so a double press or a second submit never sends the same file twice; if the earlier submit is rejected the files are offered to the next one. `onSubmit` is called in the order the submits were made, even when an earlier one is still converting its attachments; a chat page should still not return `sendMessage`'s promise from `onSubmit` (see @uifiles/chat). (10) `syncHiddenInput` is accepted for parity, but browsers cannot set a file input's value: it only clears the hidden input when the attachment list empties, and native form posts never carry the attachments. (11) PromptInputTextarea has `aria-label=\"Message\"` by default; override it via props, or pass `aria-label={undefined}` to defer to a visible `<label htmlFor>` or `aria-labelledby`, since `aria-label` outranks a native label. (12) PromptInputButton tooltips open immediately and, because Base UI tooltips carry no ARIA, their text is mirrored into a visually hidden `aria-describedby` description unless it equals the button's `aria-label` or its visible text; an empty tooltip (`\"\"` or `{ content: \"\" }`) renders no tooltip and no description. A tooltip `shortcut` is also set as `aria-keyshortcuts` on the button, so it is not lost when the description is skipped; symbols and words are translated to key names (`\"⌘K\"` → `\"Meta+K\"`, `\"Ctrl+Shift+R\"` → `\"Control+Shift+R\"`, `\"⌘⇧↵\"` → `\"Meta+Shift+Enter\"`, `\"Esc\"` → `\"Escape\"`), and an `aria-keyshortcuts` prop you pass wins. (13) PromptInputTools wraps and PromptInputSubmit never shrinks, so a long toolbar does not overlap the submit button at phone widths; clicking the empty surface of PromptInputHeader/PromptInputFooter focuses the textarea (not the model select), while clicks on anything that takes focus or handles clicks itself (buttons, links, form controls, elements with a `tabindex` other than -1, `contenteditable`, labels and ARIA widget roles) leave focus where they put it.",
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:component"
}