{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "dependencies": [
    "ai",
    "nanoid"
  ],
  "registryDependencies": [
    "https://taki-ui.com/r/button.json",
    "https://taki-ui.com/r/command.json",
    "https://taki-ui.com/r/menu.json",
    "https://taki-ui.com/r/hover-card.json",
    "https://taki-ui.com/r/input-group.json",
    "https://taki-ui.com/r/select.json",
    "https://taki-ui.com/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ai-elements/prompt-input.tsx",
      "content": "\"use client\"\n\nimport {\n  Children,\n  createContext,\n  Fragment,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ChangeEvent,\n  type ChangeEventHandler,\n  type ClipboardEventHandler,\n  type ComponentProps,\n  type FormEvent,\n  type FormEventHandler,\n  type HTMLAttributes,\n  type KeyboardEventHandler,\n  type PropsWithChildren,\n  type ReactNode,\n  type RefObject,\n} from \"react\"\nimport type { ChatStatus, FileUIPart } from \"ai\"\nimport {\n  ImageIcon,\n  Loader2Icon,\n  MicIcon,\n  PaperclipIcon,\n  PlusIcon,\n  SendIcon,\n  SquareIcon,\n  XIcon,\n} from \"lucide-react\"\nimport { nanoid } from \"nanoid\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@/registry/new-york/ui/command\"\nimport { HoverCard, HoverCardContent } from \"@/registry/new-york/ui/hover-card\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupTextarea,\n} from \"@/registry/new-york/ui/input-group\"\nimport { Menu, MenuItem, MenuTrigger } from \"@/registry/new-york/ui/menu\"\nimport { Select, SelectItem } from \"@/registry/new-york/ui/select\"\nimport { Tooltip, TooltipTrigger } from \"@/registry/new-york/ui/tooltip\"\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>(\n  null\n)\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 [attachements, setAttachements] = 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) return\n\n    setAttachements((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    setAttachements((prev) => {\n      const found = prev.find((f) => f.id === id)\n      if (found?.url) URL.revokeObjectURL(found.url)\n      return prev.filter((f) => f.id !== id)\n    })\n  }, [])\n\n  const clear = useCallback(() => {\n    setAttachements((prev) => {\n      for (const f of prev) if (f.url) URL.revokeObjectURL(f.url)\n      return []\n    })\n  }, [])\n\n  const openFileDialog = useCallback(() => {\n    openRef.current?.()\n  }, [])\n\n  const attachments = useMemo<AttachmentsContext>(\n    () => ({\n      files: attachements,\n      add,\n      remove,\n      clear,\n      openFileDialog,\n      fileInputRef,\n    }),\n    [attachements, 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 mediaType =\n    data.mediaType?.startsWith(\"image/\") && data.url ? \"image\" : \"file\"\n\n  return (\n    <div\n      className={cn(\n        \"group relative h-14 w-14 rounded-md border\",\n        className,\n        mediaType === \"image\" ? \"h-14 w-14\" : \"h-8 w-auto max-w-full\"\n      )}\n      key={data.id}\n      {...props}\n    >\n      {mediaType === \"image\" ? (\n        <img\n          alt={data.filename || \"attachment\"}\n          className=\"size-full rounded-md object-cover\"\n          height={56}\n          src={data.url}\n          width={56}\n        />\n      ) : (\n        <div className=\"text-muted-foreground flex size-full max-w-full cursor-pointer items-center justify-start gap-2 overflow-hidden px-2\">\n          <PaperclipIcon className=\"size-4 shrink-0\" />\n          <TooltipTrigger className=\"min-w-0 flex-1\" delay={400}>\n            <h4 className=\"w-full truncate text-left text-sm font-medium\">\n              {data.filename || \"Unknown file\"}\n            </h4>\n            <Tooltip>\n              <div className=\"text-muted-foreground text-xs\">\n                <h4 className=\"max-w-[240px] overflow-hidden text-left text-sm font-semibold break-words whitespace-normal\">\n                  {data.filename || \"Unknown file\"}\n                </h4>\n                {data.mediaType && <div>{data.mediaType}</div>}\n              </div>\n            </Tooltip>\n          </TooltipTrigger>\n        </div>\n      )}\n      <Button\n        aria-label=\"Remove attachment\"\n        className=\"absolute -top-1.5 -right-1.5 h-6 w-6 rounded-full opacity-0 group-hover:opacity-100\"\n        onClick={() => attachments.remove(data.id)}\n        size=\"icon\"\n        type=\"button\"\n        variant=\"outline\"\n      >\n        <XIcon className=\"h-3 w-3\" />\n      </Button>\n    </div>\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  className,\n  children,\n  ...props\n}: PromptInputAttachmentsProps) {\n  const attachments = usePromptInputAttachments()\n  const [height, setHeight] = useState(0)\n  const contentRef = useRef<HTMLDivElement>(null)\n\n  useLayoutEffect(() => {\n    const el = contentRef.current\n    if (!el) {\n      return\n    }\n    const ro = new ResizeObserver(() => {\n      setHeight(el.getBoundingClientRect().height)\n    })\n    ro.observe(el)\n    setHeight(el.getBoundingClientRect().height)\n    return () => ro.disconnect()\n  }, [])\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: Force height measurement when attachments change\n  useLayoutEffect(() => {\n    const el = contentRef.current\n    if (!el) {\n      return\n    }\n    setHeight(el.getBoundingClientRect().height)\n  }, [attachments.files.length])\n\n  if (attachments.files.length === 0) {\n    return null\n  }\n\n  return (\n    <InputGroupAddon\n      align=\"block-start\"\n      aria-live=\"polite\"\n      className={cn(\n        \"overflow-hidden transition-[height] duration-200 ease-out\",\n        className\n      )}\n      style={{ height: attachments.files.length ? height : 0 }}\n      {...props}\n    >\n      <div className=\"space-y-2 py-1\" ref={contentRef}>\n        <div className=\"flex flex-wrap gap-2\">\n          {attachments.files\n            .filter((f) => !(f.mediaType?.startsWith(\"image/\") && f.url))\n            .map((file) => (\n              <Fragment key={file.id}>{children(file)}</Fragment>\n            ))}\n        </div>\n        <div className=\"flex flex-wrap gap-2\">\n          {attachments.files\n            .filter((f) => f.mediaType?.startsWith(\"image/\") && f.url)\n            .map((file) => (\n              <Fragment key={file.id}>{children(file)}</Fragment>\n            ))}\n        </div>\n      </div>\n    </InputGroupAddon>\n  )\n}\n\nexport type PromptInputActionAddAttachmentsProps = ComponentProps<\n  typeof MenuItem\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    <MenuItem\n      {...props}\n      onAction={() => {\n        attachments.openFileDialog()\n      }}\n    >\n      <ImageIcon className=\"mr-2 size-4\" /> {label}\n    </MenuItem>\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 anchorRef = useRef<HTMLSpanElement>(null)\n  const formRef = useRef<HTMLFormElement | null>(null)\n\n  // Find nearest form to scope drag & drop\n  useEffect(() => {\n    const root = anchorRef.current?.closest(\"form\")\n    if (root instanceof HTMLFormElement) {\n      formRef.current = root\n    }\n  }, [])\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  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      if (accept.includes(\"image/*\")) {\n        return f.type.startsWith(\"image/\")\n      }\n      // NOTE: keep simple; expand as needed\n      return true\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 add = usingProvider\n    ? (files: File[] | FileList) => controller.attachments.add(files)\n    : addLocal\n\n  const remove = usingProvider\n    ? (id: string) => controller.attachments.remove(id)\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  const clear = usingProvider\n    ? () => controller.attachments.clear()\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  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\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])\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 files) {\n          if (f.url) URL.revokeObjectURL(f.url)\n        }\n      }\n    },\n    [usingProvider, files]\n  )\n\n  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {\n    if (event.currentTarget.files) {\n      add(event.currentTarget.files)\n    }\n  }\n\n  const convertBlobUrlToDataUrl = async (url: string): Promise<string> => {\n    const response = await fetch(url)\n    const blob = await response.blob()\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader()\n      reader.onloadend = () => resolve(reader.result as string)\n      reader.onerror = reject\n      reader.readAsDataURL(blob)\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          return {\n            ...item,\n            url: await convertBlobUrlToDataUrl(item.url),\n          }\n        }\n        return item\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 (error) {\n        // Don't clear on error - user may want to retry\n      }\n    })\n  }\n\n  // Render with or without local provider\n  const inner = (\n    <>\n      <span aria-hidden=\"true\" className=\"hidden\" ref={anchorRef} />\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        {...props}\n      >\n        <InputGroup>{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      e.currentTarget.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 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 MenuTrigger>\nexport const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (\n  <MenuTrigger {...props} />\n)\n\nexport type PromptInputActionMenuTriggerProps = PromptInputButtonProps\n\nexport const PromptInputActionMenuTrigger = ({\n  className,\n  children,\n  ...props\n}: PromptInputActionMenuTriggerProps) => (\n  <PromptInputButton className={className} {...props}>\n    {children ?? <PlusIcon className=\"size-4\" />}\n  </PromptInputButton>\n)\n\nexport type PromptInputActionMenuContentProps = ComponentProps<typeof Menu>\nexport const PromptInputActionMenuContent = ({\n  className,\n  ...props\n}: PromptInputActionMenuContentProps) => (\n  <Menu placement=\"bottom start\" className={cn(className)} {...props} />\n)\n\nexport type PromptInputActionMenuItemProps = ComponentProps<typeof MenuItem>\nexport const PromptInputActionMenuItem = ({\n  className,\n  ...props\n}: PromptInputActionMenuItemProps) => (\n  <MenuItem 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 = <SendIcon className=\"size-4\" />\n\n  if (status === \"submitted\") {\n    Icon = <Loader2Icon className=\"size-4 animate-spin\" />\n  } else if (status === \"streaming\") {\n    Icon = <SquareIcon className=\"size-4\" />\n  } else if (status === \"error\") {\n    Icon = <XIcon 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) => any) | null\n  onend: ((this: SpeechRecognition, ev: Event) => any) | null\n  onresult:\n    | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any)\n    | null\n  onerror:\n    | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any)\n    | null\n}\n\ninterface SpeechRecognitionEvent extends Event {\n  results: SpeechRecognitionResultList\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        const results = Array.from(event.results)\n\n        for (const result of results) {\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      className={cn(\n        \"relative transition-all duration-200\",\n        isListening && \"bg-accent text-accent-foreground animate-pulse\",\n        className\n      )}\n      disabled={!recognition}\n      onClick={toggleListening}\n      {...props}\n    >\n      <MicIcon className=\"size-4\" />\n    </PromptInputButton>\n  )\n}\n\nexport type PromptInputModelSelectProps<T extends object = { id: string }> =\n  ComponentProps<typeof Select<T>> & {\n    className?: string\n  }\n\nexport const PromptInputModelSelect = <T extends object = { id: string }>({\n  className,\n  ...props\n}: PromptInputModelSelectProps<T>) => (\n  <Select<T>\n    className={cn(\n      \"[&_button]:text-muted-foreground [&_button]:h-auto [&_button]:border-none [&_button]:bg-transparent [&_button]:px-2 [&_button]:py-1 [&_button]:font-medium [&_button]:shadow-none [&_button]:transition-colors\",\n      \"[&_button:hover]:bg-accent [&_button:hover]:text-foreground [&_button[data-pressed]]:bg-accent [&_button[data-pressed]]:text-foreground\",\n      className\n    )}\n    {...props}\n  />\n)\n\nexport type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>\n\nexport const PromptInputModelSelectItem = ({\n  className,\n  ...props\n}: PromptInputModelSelectItemProps) => (\n  <SelectItem className={cn(className)} {...props} />\n)\n\nexport type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>\n\nexport const PromptInputHoverCard = ({\n  delay = 0,\n  closeDelay = 0,\n  ...props\n}: PromptInputHoverCardProps) => (\n  <HoverCard closeDelay={closeDelay} delay={delay} {...props} />\n)\n\nexport type PromptInputHoverCardContentProps = ComponentProps<\n  typeof HoverCardContent\n>\n\nexport const PromptInputHoverCardContent = ({\n  placement = \"start\",\n  ...props\n}: PromptInputHoverCardContentProps) => (\n  <HoverCardContent placement={placement} {...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  <h3\n    className={cn(\n      \"text-muted-foreground mb-2 px-3 text-xs font-medium\",\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      \"hover:bg-accent flex items-center gap-2 px-3 py-2 text-xs\",\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"
    }
  ],
  "type": "registry:ui"
}