{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-prompt-input",
  "title": "AI: Prompt Input",
  "description": "A full chat composer: auto-growing textarea, model select, tools, attachments and speech input.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "nanoid@^5.1.16"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/command",
    "@nx-ui/dropdown-menu",
    "@nx-ui/hover-card",
    "@nx-ui/input-group",
    "@nx-ui/select",
    "@nx-ui/ai-types"
  ],
  "files": [
    {
      "path": "components/ai/prompt-input.tsx",
      "content": "\"use client\"\n\nimport {\n  type ChangeEvent,\n  type ChangeEventHandler,\n  Children,\n  type ClipboardEventHandler,\n  type ComponentProps,\n  type FormEvent,\n  type FormEventHandler,\n  Fragment,\n  type HTMLAttributes,\n  type KeyboardEventHandler,\n  type PropsWithChildren,\n  type ReactNode,\n  type RefObject,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport {\n  CornerDownLeft,\n  Image01,\n  Loading02,\n  Microphone01,\n  Paperclip,\n  Plus,\n  Square,\n  XClose,\n} from \"@untitledui/icons\"\nimport { nanoid } from \"nanoid\"\n\nimport { Button } from \"@/components/ui/button\"\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 { cn } from \"@/lib/utils\"\n\nimport type { ChatStatus, FileUIPart } from \"./types\"\n\n// ============================================================================\n// Provider Context & Types\n// ============================================================================\n\nexport type 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 type TextInputContext = {\n  value: string\n  setInput: (v: string) => void\n  clear: () => void\n}\n\nexport type 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>(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 function PromptInputProvider({\n  initialInput: initialTextInput = \"\",\n  children,\n}: PromptInputProviderProps) {\n  // ----- textInput state\n  const [textInput, setTextInput] = useState(initialTextInput)\n  const clearInput = useCallback(() => setTextInput(\"\"), [])\n\n  // ----- attachments state (global when wrapped)\n  const [attachmentFiles, setAttachmentFiles] = useState<\n    (FileUIPart & { id: string })[]\n  >([])\n  const fileInputRef = useRef<HTMLInputElement | null>(null)\n  const openRef = useRef<() => void>(() => {})\n\n  const add = useCallback((files: File[] | FileList) => {\n    const incoming = Array.from(files)\n    if (incoming.length === 0) {\n      return\n    }\n\n    setAttachmentFiles((prev) =>\n      prev.concat(\n        incoming.map((file) => ({\n          id: nanoid(),\n          type: \"file\" as const,\n          url: URL.createObjectURL(file),\n          mediaType: file.type,\n          filename: file.name,\n        }))\n      )\n    )\n  }, [])\n\n  const remove = useCallback((id: string) => {\n    setAttachmentFiles((prev) => {\n      const found = prev.find((f) => f.id === id)\n      if (found?.url) {\n        URL.revokeObjectURL(found.url)\n      }\n      return prev.filter((f) => f.id !== id)\n    })\n  }, [])\n\n  const clear = useCallback(() => {\n    setAttachmentFiles((prev) => {\n      for (const f of prev) {\n        if (f.url) {\n          URL.revokeObjectURL(f.url)\n        }\n      }\n      return []\n    })\n  }, [])\n\n  // Keep a ref to attachments for cleanup on unmount (avoids stale closure)\n  const attachmentsRef = useRef(attachmentFiles)\n  attachmentsRef.current = attachmentFiles\n\n  // Cleanup blob URLs on unmount to prevent memory leaks\n  useEffect(() => {\n    return () => {\n      for (const f of attachmentsRef.current) {\n        if (f.url) {\n          URL.revokeObjectURL(f.url)\n        }\n      }\n    }\n  }, [])\n\n  const openFileDialog = useCallback(() => {\n    openRef.current?.()\n  }, [])\n\n  const attachments = useMemo<AttachmentsContext>(\n    () => ({\n      files: attachmentFiles,\n      add,\n      remove,\n      clear,\n      openFileDialog,\n      fileInputRef,\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      textInput: {\n        value: textInput,\n        setInput: setTextInput,\n        clear: clearInput,\n      },\n      attachments,\n      __registerFileInput,\n    }),\n    [textInput, clearInput, attachments, __registerFileInput]\n  )\n\n  return (\n    <PromptInputController.Provider value={controller}>\n      <ProviderAttachmentsContext.Provider value={attachments}>\n        {children}\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  // Dual-mode: prefer provider if present, otherwise use local\n  const provider = useOptionalProviderAttachments()\n  const local = useContext(LocalAttachmentsContext)\n  const context = provider ?? local\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 PromptInputAttachmentProps = HTMLAttributes<HTMLDivElement> & {\n  data: FileUIPart & { id: string }\n  className?: string\n}\n\nexport function PromptInputAttachment({\n  data,\n  className,\n  ...props\n}: PromptInputAttachmentProps) {\n  const attachments = usePromptInputAttachments()\n\n  const filename = data.filename || \"\"\n\n  const mediaType =\n    data.mediaType?.startsWith(\"image/\") && data.url ? \"image\" : \"file\"\n  const isImage = mediaType === \"image\"\n\n  const attachmentLabel = filename || (isImage ? \"Image\" : \"Attachment\")\n\n  return (\n    <PromptInputHoverCard>\n      <HoverCardTrigger\n        render={\n          <div\n            className={cn(\n              \"group relative flex h-8 cursor-pointer select-none items-center gap-1.5 rounded-md border border-border px-1.5 font-medium text-sm transition-all hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n              className\n            )}\n            key={data.id}\n            {...props}\n          >\n            <div className=\"relative size-5 shrink-0\">\n              <div className=\"absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded bg-background transition-opacity group-hover:opacity-0\">\n                {isImage ? (\n                  // eslint-disable-next-line @next/next/no-img-element\n                  <img\n                    alt={filename || \"attachment\"}\n                    className=\"size-5 object-cover\"\n                    height={20}\n                    src={data.url}\n                    width={20}\n                  />\n                ) : (\n                  <div className=\"flex size-5 items-center justify-center text-muted-foreground\">\n                    <Paperclip className=\"size-3\" />\n                  </div>\n                )}\n              </div>\n              <Button\n                aria-label=\"Remove attachment\"\n                className=\"absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5\"\n                onClick={(e) => {\n                  e.stopPropagation()\n                  attachments.remove(data.id)\n                }}\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <XClose />\n                <span className=\"sr-only\">Remove</span>\n              </Button>\n            </div>\n\n            <span className=\"flex-1 truncate\">{attachmentLabel}</span>\n          </div>\n        }\n      />\n      <PromptInputHoverCardContent className=\"w-auto p-2\">\n        <div className=\"w-auto space-y-3\">\n          {isImage && (\n            <div className=\"flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border\">\n              {/* eslint-disable-next-line @next/next/no-img-element */}\n              <img\n                alt={filename || \"attachment preview\"}\n                className=\"max-h-full max-w-full object-contain\"\n                height={384}\n                src={data.url}\n                width={448}\n              />\n            </div>\n          )}\n          <div className=\"flex items-center gap-2.5\">\n            <div className=\"min-w-0 flex-1 space-y-1 px-0.5\">\n              <h4 className=\"truncate font-semibold text-sm leading-none\">\n                {filename || (isImage ? \"Image\" : \"Attachment\")}\n              </h4>\n              {data.mediaType && (\n                <p className=\"truncate font-mono text-muted-foreground text-xs\">\n                  {data.mediaType}\n                </p>\n              )}\n            </div>\n          </div>\n        </div>\n      </PromptInputHoverCardContent>\n    </PromptInputHoverCard>\n  )\n}\n\nexport type PromptInputAttachmentsProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  children: (attachment: FileUIPart & { id: string }) => ReactNode\n}\n\nexport function PromptInputAttachments({\n  children,\n  className,\n  ...props\n}: PromptInputAttachmentsProps) {\n  const attachments = usePromptInputAttachments()\n\n  if (!attachments.files.length) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(\"flex flex-wrap items-center gap-2 p-3 w-full\", className)}\n      {...props}\n    >\n      {attachments.files.map((file) => (\n        <Fragment key={file.id}>{children(file)}</Fragment>\n      ))}\n    </div>\n  )\n}\n\nexport type PromptInputActionAddAttachmentsProps = ComponentProps<\n  typeof DropdownMenuItem\n> & {\n  label?: string\n}\n\nexport const PromptInputActionAddAttachments = ({\n  label = \"Add photos or files\",\n  ...props\n}: PromptInputActionAddAttachmentsProps) => {\n  const attachments = usePromptInputAttachments()\n\n  return (\n    <DropdownMenuItem\n      {...props}\n      onSelect={(e) => {\n        e.preventDefault()\n        attachments.openFileDialog()\n      }}\n    >\n      <Image01 className=\"mr-2 size-4\" /> {label}\n    </DropdownMenuItem>\n  )\n}\n\nexport type PromptInputMessage = {\n  text: string\n  files: FileUIPart[]\n}\n\nexport type PromptInputProps = Omit<\n  HTMLAttributes<HTMLFormElement>,\n  \"onSubmit\" | \"onError\"\n> & {\n  accept?: string // e.g., \"image/*\" or leave undefined for any\n  multiple?: boolean\n  // When true, accepts drops anywhere on document. Default false (opt-in).\n  globalDrop?: boolean\n  // Render a hidden input with given name and keep it in sync for native form posts. Default false.\n  syncHiddenInput?: boolean\n  // Minimal constraints\n  maxFiles?: number\n  maxFileSize?: number // bytes\n  onError?: (err: {\n    code: \"max_files\" | \"max_file_size\" | \"accept\"\n    message: string\n  }) => void\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>\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\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<(FileUIPart & { id: string })[]>([])\n  const files = usingProvider ? controller.attachments.files : items\n\n  // Keep a ref to files for cleanup on unmount (avoids stale closure)\n  const filesRef = useRef(files)\n  filesRef.current = files\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())\n        .filter(Boolean)\n\n      return patterns.some((pattern) => {\n        if (pattern.endsWith(\"/*\")) {\n          const prefix = pattern.slice(0, -1) // e.g: image/* -> image/\n          return f.type.startsWith(prefix)\n        }\n        return f.type === pattern\n      })\n    },\n    [accept]\n  )\n\n  const addLocal = useCallback(\n    (fileList: File[] | FileList) => {\n      const incoming = Array.from(fileList)\n      const accepted = incoming.filter((f) => matchesAccept(f))\n      if (incoming.length && accepted.length === 0) {\n        onError?.({\n          code: \"accept\",\n          message: \"No files match the accepted types.\",\n        })\n        return\n      }\n      const withinSize = (f: File) =>\n        maxFileSize ? f.size <= maxFileSize : true\n      const sized = accepted.filter(withinSize)\n      if (accepted.length > 0 && sized.length === 0) {\n        onError?.({\n          code: \"max_file_size\",\n          message: \"All files exceed the maximum size.\",\n        })\n        return\n      }\n\n      setItems((prev) => {\n        const capacity =\n          typeof maxFiles === \"number\"\n            ? Math.max(0, maxFiles - prev.length)\n            : undefined\n        const capped =\n          typeof capacity === \"number\" ? sized.slice(0, capacity) : sized\n        if (typeof capacity === \"number\" && sized.length > capacity) {\n          onError?.({\n            code: \"max_files\",\n            message: \"Too many files. Some were not added.\",\n          })\n        }\n        const next: (FileUIPart & { id: string })[] = []\n        for (const file of capped) {\n          next.push({\n            id: nanoid(),\n            type: \"file\",\n            url: URL.createObjectURL(file),\n            mediaType: file.type,\n            filename: file.name,\n          })\n        }\n        return prev.concat(next)\n      })\n    },\n    [matchesAccept, maxFiles, maxFileSize, onError]\n  )\n\n  const removeLocal = useCallback(\n    (id: string) =>\n      setItems((prev) => {\n        const found = prev.find((file) => file.id === id)\n        if (found?.url) {\n          URL.revokeObjectURL(found.url)\n        }\n        return prev.filter((file) => file.id !== id)\n      }),\n    []\n  )\n\n  const clearLocal = useCallback(\n    () =>\n      setItems((prev) => {\n        for (const file of prev) {\n          if (file.url) {\n            URL.revokeObjectURL(file.url)\n          }\n        }\n        return []\n      }),\n    []\n  )\n\n  const add = usingProvider ? controller.attachments.add : addLocal\n  const remove = usingProvider ? controller.attachments.remove : removeLocal\n  const clear = usingProvider ? controller.attachments.clear : clearLocal\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) return\n    controller.__registerFileInput(inputRef, () => inputRef.current?.click())\n  }, [usingProvider, controller])\n\n  // Note: File input cannot be programmatically set for security reasons\n  // The syncHiddenInput prop is no longer functional\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) return\n    if (globalDrop) return // when global drop is on, let the document-level handler own drops\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) return\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        for (const f of filesRef.current) {\n          if (f.url) URL.revokeObjectURL(f.url)\n        }\n      }\n    },\n    [usingProvider]\n  )\n\n  const handleChange: ChangeEventHandler<HTMLInputElement> = (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\n  const convertBlobUrlToDataUrl = async (\n    url: string\n  ): Promise<string | null> => {\n    try {\n      const response = await fetch(url)\n      const blob = await response.blob()\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\n  const ctx = useMemo<AttachmentsContext>(\n    () => ({\n      files: files.map((item) => ({ ...item, id: item.id })),\n      add,\n      remove,\n      clear,\n      openFileDialog,\n      fileInputRef: inputRef,\n    }),\n    [files, add, remove, clear, openFileDialog]\n  )\n\n  const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {\n    event.preventDefault()\n\n    const form = event.currentTarget\n    const text = usingProvider\n      ? controller.textInput.value\n      : (() => {\n          const formData = new FormData(form)\n          return (formData.get(\"message\") as string) || \"\"\n        })()\n\n    // Reset form immediately after capturing text to avoid race condition\n    // where user input during async blob conversion would be lost\n    if (!usingProvider) {\n      form.reset()\n    }\n\n    // Convert blob URLs to data URLs asynchronously\n    Promise.all(\n      files.map(async ({ id, ...item }) => {\n        if (item.url && 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      .then((convertedFiles: FileUIPart[]) => {\n        try {\n          const result = onSubmit({ text, files: convertedFiles }, event)\n\n          // Handle both sync and async onSubmit\n          if (result instanceof Promise) {\n            result\n              .then(() => {\n                clear()\n                if (usingProvider) {\n                  controller.textInput.clear()\n                }\n              })\n              .catch(() => {\n                // Don't clear on error - user may want to retry\n              })\n          } else {\n            // Sync function completed without throwing, clear attachments\n            clear()\n            if (usingProvider) {\n              controller.textInput.clear()\n            }\n          }\n        } catch {\n          // Don't clear on error - user may want to retry\n        }\n      })\n      .catch(() => {\n        // Don't clear on error - user may want to retry\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  return usingProvider ? (\n    inner\n  ) : (\n    <LocalAttachmentsContext.Provider value={ctx}>\n      {inner}\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  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 handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {\n    if (e.key === \"Enter\") {\n      if (isComposing || e.nativeEvent.isComposing) {\n        return\n      }\n      if (e.shiftKey) {\n        return\n      }\n      e.preventDefault()\n\n      // Check if the submit button is disabled before submitting\n      const form = e.currentTarget.form\n      const submitButton = form?.querySelector(\n        'button[type=\"submit\"]'\n      ) as HTMLButtonElement | null\n      if (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\n  const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (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\n  const controlledProps = controller\n    ? {\n        value: controller.textInput.value,\n        onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n          controller.textInput.setInput(e.currentTarget.value)\n          onChange?.(e)\n        },\n      }\n    : {\n        onChange,\n      }\n\n  return (\n    <InputGroupTextarea\n      className={cn(\"field-sizing-content max-h-48 min-h-16\", className)}\n      name=\"message\"\n      onCompositionEnd={() => setIsComposing(false)}\n      onCompositionStart={() => setIsComposing(true)}\n      onKeyDown={handleKeyDown}\n      onPaste={handlePaste}\n      placeholder={placeholder}\n      {...props}\n      {...controlledProps}\n    />\n  )\n}\n\nexport type PromptInputHeaderProps = Omit<\n  ComponentProps<typeof InputGroupAddon>,\n  \"align\"\n>\n\nexport const PromptInputHeader = ({\n  className,\n  ...props\n}: PromptInputHeaderProps) => (\n  <InputGroupAddon\n    align=\"block-end\"\n    className={cn(\"order-first flex-wrap gap-1\", className)}\n    {...props}\n  />\n)\n\nexport type PromptInputFooterProps = Omit<\n  ComponentProps<typeof InputGroupAddon>,\n  \"align\"\n>\n\nexport const PromptInputFooter = ({\n  className,\n  ...props\n}: PromptInputFooterProps) => (\n  <InputGroupAddon\n    align=\"block-end\"\n    className={cn(\"justify-between gap-1\", className)}\n    {...props}\n  />\n)\n\nexport type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>\n\nexport const PromptInputTools = ({\n  className,\n  ...props\n}: PromptInputToolsProps) => (\n  <div className={cn(\"flex items-center gap-1\", className)} {...props} />\n)\n\nexport type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>\n\nexport const PromptInputButton = ({\n  variant = \"ghost\",\n  className,\n  size,\n  ...props\n}: PromptInputButtonProps) => {\n  const newSize =\n    size ?? (Children.count(props.children) > 1 ? \"sm\" : \"icon-sm\")\n\n  return (\n    <InputGroupButton\n      className={cn(className)}\n      size={newSize}\n      type=\"button\"\n      variant={variant}\n      {...props}\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\n        aria-label=\"Add attachment\"\n        className={className}\n        {...props}\n      >\n        {children ?? <Plus 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}\n\nexport const PromptInputSubmit = ({\n  className,\n  variant = \"default\",\n  size = \"icon-sm\",\n  status,\n  children,\n  ...props\n}: PromptInputSubmitProps) => {\n  let Icon = <CornerDownLeft className=\"size-4\" />\n\n  if (status === \"submitted\") {\n    Icon = <Loading02 className=\"size-4 animate-spin\" />\n  } else if (status === \"streaming\") {\n    Icon = <Square className=\"size-4\" />\n  } else if (status === \"error\") {\n    Icon = <XClose className=\"size-4\" />\n  }\n\n  return (\n    <InputGroupButton\n      aria-label=\"Submit\"\n      className={cn(className)}\n      size={size}\n      type=\"submit\"\n      variant={variant}\n      {...props}\n    >\n      {children ?? Icon}\n    </InputGroupButton>\n  )\n}\n\ninterface SpeechRecognition extends EventTarget {\n  continuous: boolean\n  interimResults: boolean\n  lang: string\n  start(): void\n  stop(): void\n  onstart: ((this: SpeechRecognition, ev: Event) => unknown) | null\n  onend: ((this: SpeechRecognition, ev: Event) => unknown) | null\n  onresult:\n    | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => unknown)\n    | null\n  onerror:\n    | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => unknown)\n    | null\n}\n\ninterface SpeechRecognitionEvent extends Event {\n  results: SpeechRecognitionResultList\n  resultIndex: number\n}\n\ntype SpeechRecognitionResultList = {\n  readonly length: number\n  item(index: number): SpeechRecognitionResult\n  [index: number]: SpeechRecognitionResult\n}\n\ntype SpeechRecognitionResult = {\n  readonly length: number\n  item(index: number): SpeechRecognitionAlternative\n  [index: number]: SpeechRecognitionAlternative\n  isFinal: boolean\n}\n\ntype SpeechRecognitionAlternative = {\n  transcript: string\n  confidence: number\n}\n\ninterface SpeechRecognitionErrorEvent extends Event {\n  error: string\n}\n\ndeclare global {\n  interface Window {\n    SpeechRecognition: {\n      new (): SpeechRecognition\n    }\n    webkitSpeechRecognition: {\n      new (): SpeechRecognition\n    }\n  }\n}\n\nexport type PromptInputSpeechButtonProps = ComponentProps<\n  typeof PromptInputButton\n> & {\n  textareaRef?: RefObject<HTMLTextAreaElement | null>\n  onTranscriptionChange?: (text: string) => void\n}\n\nexport const PromptInputSpeechButton = ({\n  className,\n  textareaRef,\n  onTranscriptionChange,\n  ...props\n}: PromptInputSpeechButtonProps) => {\n  const [isListening, setIsListening] = useState(false)\n  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null)\n  const recognitionRef = useRef<SpeechRecognition | null>(null)\n\n  useEffect(() => {\n    if (\n      typeof window !== \"undefined\" &&\n      (\"SpeechRecognition\" in window || \"webkitSpeechRecognition\" in window)\n    ) {\n      const SpeechRecognition =\n        window.SpeechRecognition || window.webkitSpeechRecognition\n      const speechRecognition = new SpeechRecognition()\n\n      speechRecognition.continuous = true\n      speechRecognition.interimResults = true\n      speechRecognition.lang = \"en-US\"\n\n      speechRecognition.onstart = () => {\n        setIsListening(true)\n      }\n\n      speechRecognition.onend = () => {\n        setIsListening(false)\n      }\n\n      speechRecognition.onresult = (event) => {\n        let finalTranscript = \"\"\n\n        for (let i = event.resultIndex; i < event.results.length; i++) {\n          const result = event.results[i]\n          if (result.isFinal) {\n            finalTranscript += result[0]?.transcript ?? \"\"\n          }\n        }\n\n        if (finalTranscript && textareaRef?.current) {\n          const textarea = textareaRef.current\n          const currentValue = textarea.value\n          const newValue =\n            currentValue + (currentValue ? \" \" : \"\") + finalTranscript\n\n          textarea.value = newValue\n          textarea.dispatchEvent(new Event(\"input\", { bubbles: true }))\n          onTranscriptionChange?.(newValue)\n        }\n      }\n\n      speechRecognition.onerror = (event) => {\n        console.error(\"Speech recognition error:\", event.error)\n        setIsListening(false)\n      }\n\n      recognitionRef.current = speechRecognition\n      setRecognition(speechRecognition)\n    }\n\n    return () => {\n      if (recognitionRef.current) {\n        recognitionRef.current.stop()\n      }\n    }\n  }, [textareaRef, onTranscriptionChange])\n\n  const toggleListening = useCallback(() => {\n    if (!recognition) {\n      return\n    }\n\n    if (isListening) {\n      recognition.stop()\n    } else {\n      recognition.start()\n    }\n  }, [recognition, isListening])\n\n  return (\n    <PromptInputButton\n      aria-label=\"Dictate message\"\n      className={cn(\n        \"relative transition-all duration-200\",\n        isListening && \"animate-pulse bg-accent text-accent-foreground\",\n        className\n      )}\n      disabled={!recognition}\n      onClick={toggleListening}\n      {...props}\n    >\n      <Microphone01 className=\"size-4\" />\n    </PromptInputButton>\n  )\n}\n\nexport type PromptInputSelectProps = ComponentProps<typeof Select>\n\nexport const PromptInputSelect = (props: PromptInputSelectProps) => (\n  <Select {...props} />\n)\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  ...props\n}: PromptInputSelectValueProps) => <SelectValue {...props} />\n\nexport type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>\n\nexport const PromptInputHoverCard = (props: PromptInputHoverCardProps) => (\n  <HoverCard {...props} />\n)\n\nexport type PromptInputHoverCardTriggerProps = ComponentProps<\n  typeof HoverCardTrigger\n>\n\nexport const PromptInputHoverCardTrigger = (\n  props: PromptInputHoverCardTriggerProps\n) => <HoverCardTrigger {...props} />\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 = ({ className, ...props }: PromptInputTabProps) => (\n  <div className={cn(className)} {...props} />\n)\n\nexport type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>\n\nexport const PromptInputTabLabel = ({\n  className,\n  ...props\n}: PromptInputTabLabelProps) => (\n  <h3\n    className={cn(\n      \"mb-2 px-3 font-medium text-muted-foreground text-xs\",\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:ui",
      "target": "components/ai/prompt-input.tsx"
    }
  ],
  "type": "registry:ui"
}