{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-chat1",
  "title": "Ai chat",
  "description": "Chat home with a sidebar of grouped chat history, a header model menu with copy link and share buttons, a composer with research, image and reasoning toggles, a connect tools row and three starter cards.",
  "dependencies": [
    "ai@7",
    "lucide-react@0",
    "nanoid@6",
    "sonner@2"
  ],
  "registryDependencies": [
    "avatar",
    "button",
    "command",
    "dropdown-menu",
    "hover-card",
    "input-group",
    "item",
    "kbd",
    "select",
    "separator",
    "sidebar",
    "sonner",
    "spinner",
    "toggle-group",
    "tooltip"
  ],
  "files": [
    {
      "path": "contents/blocks/application-ui/ai-chat/1/index.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n  ArrowUpIcon,\n  AtomIcon,\n  BookOpenIcon,\n  BrainIcon,\n  CalendarIcon,\n  ChevronDownIcon,\n  ChevronRightIcon,\n  ChevronsUpDownIcon,\n  CompassIcon,\n  FileTextIcon,\n  GithubIcon,\n  GlobeIcon,\n  ImageIcon,\n  LayoutTemplateIcon,\n  LightbulbIcon,\n  LinkIcon,\n  LogOutIcon,\n  MailIcon,\n  MicIcon,\n  MoreHorizontalIcon,\n  PaperclipIcon,\n  PenLineIcon,\n  PlusIcon,\n  SettingsIcon,\n  Share2Icon,\n  SparklesIcon,\n  Trash2Icon,\n} from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nimport {\n  Attachment,\n  AttachmentInfo,\n  AttachmentPreview,\n  AttachmentRemove,\n  Attachments,\n} from \"@/components/ui/attachments\";\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarGroup,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemMedia,\n  ItemTitle,\n} from \"@/components/ui/item\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputButton,\n  PromptInputFooter,\n  PromptInputHeader,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  PromptInputTools,\n  usePromptInputAttachments,\n  type PromptInputMessage,\n} from \"@/components/ui/prompt-input\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarProvider,\n  SidebarTrigger,\n} from \"@/components/ui/sidebar\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\nconst navItems = [\n  { title: \"Explore\", icon: CompassIcon },\n  { title: \"Knowledge Base\", icon: BookOpenIcon },\n  { title: \"Templates\", icon: LayoutTemplateIcon },\n];\n\nconst history = [\n  {\n    label: \"Today\",\n    chats: [\n      \"Summarize the Q3 board deck\",\n      \"Polite follow up for the Acme proposal\",\n      \"Startup ideas for pet owners\",\n    ],\n  },\n  {\n    label: \"Yesterday\",\n    chats: [\n      \"Explain vector databases simply\",\n      \"Translate the onboarding guide to Spanish\",\n      \"Five captions for the spring launch\",\n      \"Debug a Python retry loop\",\n    ],\n  },\n  {\n    label: \"September\",\n    chats: [\"Outline a blog post on pricing\", \"Weekly workout plan\"],\n  },\n];\n\nconst models = [\"Nexa Core 2\", \"Nexa Swift\", \"Nexa Reason\"];\n\nconst integrations = [\n  { name: \"Gmail\", icon: MailIcon },\n  { name: \"Calendar\", icon: CalendarIcon },\n  { name: \"Docs\", icon: FileTextIcon },\n  { name: \"GitHub\", icon: GithubIcon },\n];\n\nconst starters = [\n  {\n    title: \"Summarize text\",\n    description: \"Turn long articles into short, easy summaries.\",\n    prompt: \"Summarize this article in five bullet points: \",\n    icon: FileTextIcon,\n  },\n  {\n    title: \"Creative writing\",\n    description: \"Draft stories, blog posts or fresh content ideas.\",\n    prompt: \"Write a short blog intro about \",\n    icon: PenLineIcon,\n  },\n  {\n    title: \"Answer questions\",\n    description: \"Ask about facts, advice or anything in between.\",\n    prompt: \"Help me understand \",\n    icon: BrainIcon,\n  },\n];\n\nfunction AppSidebar() {\n  const [active, setActive] = useState(history[0].chats[0]);\n\n  return (\n    <Sidebar>\n      <SidebarHeader className=\"gap-3 p-3\">\n        <div className=\"flex items-center gap-2 px-1 pt-1\">\n          <div className=\"bg-primary text-primary-foreground flex size-7 items-center justify-center rounded-lg\">\n            <SparklesIcon className=\"size-4\" />\n          </div>\n          <span className=\"font-semibold\">Nexa</span>\n        </div>\n        <Button\n          className=\"justify-between\"\n          onClick={() => toast(\"New chat started\")}\n        >\n          <span className=\"flex items-center gap-2\">\n            <PlusIcon />\n            New chat\n          </span>\n          <Kbd className=\"bg-primary-foreground/15 text-primary-foreground\">\n            ⌘N\n          </Kbd>\n        </Button>\n      </SidebarHeader>\n      <SidebarContent>\n        <SidebarGroup>\n          <SidebarGroupContent>\n            <SidebarMenu>\n              {navItems.map((item) => (\n                <SidebarMenuItem key={item.title}>\n                  <SidebarMenuButton>\n                    <item.icon />\n                    <span>{item.title}</span>\n                  </SidebarMenuButton>\n                </SidebarMenuItem>\n              ))}\n            </SidebarMenu>\n          </SidebarGroupContent>\n        </SidebarGroup>\n        {history.map((group) => (\n          <SidebarGroup key={group.label}>\n            <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n            <SidebarGroupContent>\n              <SidebarMenu>\n                {group.chats.map((chat) => (\n                  <SidebarMenuItem key={chat}>\n                    <SidebarMenuButton\n                      isActive={active === chat}\n                      onClick={() => setActive(chat)}\n                    >\n                      <span>{chat}</span>\n                    </SidebarMenuButton>\n                    <DropdownMenu>\n                      <DropdownMenuTrigger asChild>\n                        <SidebarMenuAction showOnHover>\n                          <MoreHorizontalIcon />\n                          <span className=\"sr-only\">More</span>\n                        </SidebarMenuAction>\n                      </DropdownMenuTrigger>\n                      <DropdownMenuContent side=\"right\" align=\"start\">\n                        <DropdownMenuItem>\n                          <Share2Icon />\n                          Share\n                        </DropdownMenuItem>\n                        <DropdownMenuItem variant=\"destructive\">\n                          <Trash2Icon />\n                          Delete\n                        </DropdownMenuItem>\n                      </DropdownMenuContent>\n                    </DropdownMenu>\n                  </SidebarMenuItem>\n                ))}\n              </SidebarMenu>\n            </SidebarGroupContent>\n          </SidebarGroup>\n        ))}\n      </SidebarContent>\n      <SidebarFooter>\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <SidebarMenuButton>\n              <SettingsIcon />\n              <span>Settings</span>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n          <SidebarMenuItem>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <SidebarMenuButton size=\"lg\">\n                  <Avatar className=\"size-8\">\n                    <AvatarImage\n                      src=\"https://i.pravatar.cc/80?img=12\"\n                      alt=\"Daniel Reyes\"\n                    />\n                    <AvatarFallback>DR</AvatarFallback>\n                  </Avatar>\n                  <div className=\"grid flex-1 text-left leading-tight\">\n                    <span className=\"truncate text-sm font-medium\">\n                      Daniel Reyes\n                    </span>\n                    <span className=\"text-muted-foreground truncate text-xs\">\n                      Free plan\n                    </span>\n                  </div>\n                  <ChevronsUpDownIcon className=\"text-muted-foreground ml-auto\" />\n                </SidebarMenuButton>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent side=\"top\" align=\"start\" className=\"w-56\">\n                <DropdownMenuLabel>daniel@nexa.ai</DropdownMenuLabel>\n                <DropdownMenuSeparator />\n                <DropdownMenuItem>\n                  <SparklesIcon />\n                  Upgrade plan\n                </DropdownMenuItem>\n                <DropdownMenuItem>\n                  <LogOutIcon />\n                  Sign out\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </SidebarMenuItem>\n        </SidebarMenu>\n      </SidebarFooter>\n    </Sidebar>\n  );\n}\n\nfunction PromptAttachments() {\n  const attachments = usePromptInputAttachments();\n\n  if (attachments.files.length === 0) return null;\n\n  return (\n    <PromptInputHeader>\n      <Attachments variant=\"inline\">\n        {attachments.files.map((file) => (\n          <Attachment\n            key={file.id}\n            data={file}\n            onRemove={() => attachments.remove(file.id)}\n          >\n            <AttachmentPreview />\n            <AttachmentInfo className=\"max-w-40\" />\n            <AttachmentRemove />\n          </Attachment>\n        ))}\n      </Attachments>\n    </PromptInputHeader>\n  );\n}\n\nfunction AttachButton() {\n  const attachments = usePromptInputAttachments();\n\n  return (\n    <PromptInputButton\n      tooltip=\"Attach files\"\n      aria-label=\"Attach files\"\n      onClick={attachments.openFileDialog}\n    >\n      <PaperclipIcon />\n    </PromptInputButton>\n  );\n}\n\nexport default function AiChat() {\n  const [model, setModel] = useState(models[0]);\n  const [text, setText] = useState(\"\");\n  const [modes, setModes] = useState<string[]>([\"research\"]);\n  const [webSearch, setWebSearch] = useState(false);\n\n  const handleSubmit = (message: PromptInputMessage) => {\n    if (!message.text.trim() && message.files.length === 0) return;\n    setText(\"\");\n    toast(\"Message sent\", { description: `Answering with ${model}` });\n  };\n\n  return (\n    <SidebarProvider className=\"h-svh min-h-0 overflow-hidden\">\n      <AppSidebar />\n      <SidebarInset className=\"min-h-0 min-w-0 overflow-hidden\">\n        <header className=\"flex h-14 shrink-0 items-center gap-2 border-b px-4\">\n          <SidebarTrigger className=\"-ml-1\" />\n          <Separator\n            orientation=\"vertical\"\n            className=\"mr-1 h-4 data-vertical:self-center\"\n          />\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button variant=\"ghost\" className=\"px-2 font-medium\">\n                {model}\n                <ChevronDownIcon className=\"text-muted-foreground\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"start\" className=\"w-48\">\n              <DropdownMenuLabel>Model</DropdownMenuLabel>\n              <DropdownMenuRadioGroup value={model} onValueChange={setModel}>\n                {models.map((item) => (\n                  <DropdownMenuRadioItem key={item} value={item}>\n                    {item}\n                  </DropdownMenuRadioItem>\n                ))}\n              </DropdownMenuRadioGroup>\n            </DropdownMenuContent>\n          </DropdownMenu>\n          <div className=\"ml-auto flex items-center gap-1\">\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  aria-label=\"Copy link\"\n                  onClick={() => toast(\"Link copied\")}\n                >\n                  <LinkIcon />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>Copy link</TooltipContent>\n            </Tooltip>\n            <Button\n              variant=\"outline\"\n              onClick={() => toast(\"Share link created\")}\n            >\n              <Share2Icon />\n              Share\n            </Button>\n          </div>\n        </header>\n\n        <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto\">\n          <div className=\"mx-auto flex w-full max-w-3xl flex-1 flex-col items-center justify-center gap-8 px-4 py-10\">\n            <div className=\"flex flex-col items-center gap-5 text-center\">\n              <Button variant=\"outline\" size=\"sm\" className=\"rounded-full\">\n                <SparklesIcon />\n                <span>\n                  <span className=\"text-primary font-medium\">Upgrade</span> for\n                  full access\n                </span>\n              </Button>\n              <h1 className=\"text-2xl font-semibold tracking-tight sm:text-3xl\">\n                Let&apos;s start a smart conversation\n              </h1>\n            </div>\n\n            <div className=\"bg-muted/60 w-full rounded-3xl\">\n              <PromptInput\n                multiple\n                onSubmit={handleSubmit}\n                className=\"*:data-[slot=input-group]:bg-card *:data-[slot=input-group]:border-border *:data-[slot=input-group]:rounded-3xl *:data-[slot=input-group]:shadow-xs\"\n              >\n                <PromptAttachments />\n                <PromptInputBody>\n                  <PromptInputTextarea\n                    value={text}\n                    onChange={(event) => setText(event.currentTarget.value)}\n                    placeholder=\"Ask me anything...\"\n                    className=\"px-4 pt-4\"\n                  />\n                </PromptInputBody>\n                <PromptInputFooter className=\"p-3\">\n                  <ToggleGroup\n                    type=\"multiple\"\n                    variant=\"outline\"\n                    size=\"sm\"\n                    value={modes}\n                    onValueChange={setModes}\n                  >\n                    <ToggleGroupItem value=\"research\" className=\"px-3\">\n                      <AtomIcon />\n                      <span className=\"hidden sm:inline\">Deep research</span>\n                    </ToggleGroupItem>\n                    <ToggleGroupItem value=\"image\" aria-label=\"Create image\">\n                      <ImageIcon />\n                    </ToggleGroupItem>\n                    <ToggleGroupItem value=\"reason\" aria-label=\"Think longer\">\n                      <LightbulbIcon />\n                    </ToggleGroupItem>\n                  </ToggleGroup>\n                  <PromptInputTools>\n                    <PromptInputButton\n                      tooltip=\"Search the web\"\n                      aria-label=\"Search the web\"\n                      aria-pressed={webSearch}\n                      variant={webSearch ? \"secondary\" : \"ghost\"}\n                      onClick={() => setWebSearch((value) => !value)}\n                    >\n                      <GlobeIcon />\n                    </PromptInputButton>\n                    <AttachButton />\n                    <PromptInputButton\n                      tooltip=\"Voice input\"\n                      aria-label=\"Voice input\"\n                    >\n                      <MicIcon />\n                    </PromptInputButton>\n                    <PromptInputSubmit className=\"rounded-full\">\n                      <ArrowUpIcon />\n                    </PromptInputSubmit>\n                  </PromptInputTools>\n                </PromptInputFooter>\n              </PromptInput>\n              <Item\n                asChild\n                size=\"sm\"\n                className=\"text-muted-foreground hover:text-foreground rounded-none rounded-b-3xl px-5 text-left\"\n              >\n                <button\n                  type=\"button\"\n                  onClick={() => toast(\"Integrations opened\")}\n                >\n                  <ItemContent className=\"truncate\">\n                    Connect your tools to Nexa\n                  </ItemContent>\n                  <ItemActions>\n                    <AvatarGroup className=\"*:data-[slot=avatar]:size-6\">\n                      {integrations.map((item) => (\n                        <Avatar key={item.name}>\n                          <AvatarFallback className=\"bg-background\">\n                            <item.icon className=\"size-3\" />\n                          </AvatarFallback>\n                        </Avatar>\n                      ))}\n                    </AvatarGroup>\n                    <ChevronRightIcon className=\"size-4\" />\n                  </ItemActions>\n                </button>\n              </Item>\n            </div>\n\n            <div className=\"grid w-full gap-3 sm:grid-cols-3\">\n              {starters.map((item) => (\n                <Item\n                  key={item.title}\n                  asChild\n                  variant=\"outline\"\n                  className=\"hover:bg-muted/50 flex-col items-start gap-3 text-left\"\n                >\n                  <button type=\"button\" onClick={() => setText(item.prompt)}>\n                    <ItemMedia variant=\"icon\" className=\"text-muted-foreground\">\n                      <item.icon />\n                    </ItemMedia>\n                    <ItemContent>\n                      <ItemTitle>{item.title}</ItemTitle>\n                      <ItemDescription>{item.description}</ItemDescription>\n                    </ItemContent>\n                  </button>\n                </Item>\n              ))}\n            </div>\n          </div>\n          <p className=\"text-muted-foreground shrink-0 px-4 pb-4 text-center text-xs\">\n            Nexa can make mistakes. Check important info.\n          </p>\n        </div>\n      </SidebarInset>\n      <Toaster />\n    </SidebarProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/ai-chat1.tsx"
    },
    {
      "path": "components/ui/attachments.tsx",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport { cn } from \"@/lib/utils\";\nimport type { FileUIPart, SourceDocumentUIPart } from \"ai\";\nimport {\n  FileTextIcon,\n  GlobeIcon,\n  ImageIcon,\n  Music2Icon,\n  PaperclipIcon,\n  VideoIcon,\n  XIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type AttachmentData =\n  | (FileUIPart & { id: string })\n  | (SourceDocumentUIPart & { id: string });\n\nexport type AttachmentMediaCategory =\n  | \"image\"\n  | \"video\"\n  | \"audio\"\n  | \"document\"\n  | \"source\"\n  | \"unknown\";\n\nexport type AttachmentVariant = \"grid\" | \"inline\" | \"list\";\n\nconst mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {\n  audio: Music2Icon,\n  document: FileTextIcon,\n  image: ImageIcon,\n  source: GlobeIcon,\n  unknown: PaperclipIcon,\n  video: VideoIcon,\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nexport const getMediaCategory = (\n  data: AttachmentData\n): AttachmentMediaCategory => {\n  if (data.type === \"source-document\") {\n    return \"source\";\n  }\n\n  const mediaType = data.mediaType ?? \"\";\n\n  if (mediaType.startsWith(\"image/\")) {\n    return \"image\";\n  }\n  if (mediaType.startsWith(\"video/\")) {\n    return \"video\";\n  }\n  if (mediaType.startsWith(\"audio/\")) {\n    return \"audio\";\n  }\n  if (mediaType.startsWith(\"application/\") || mediaType.startsWith(\"text/\")) {\n    return \"document\";\n  }\n\n  return \"unknown\";\n};\n\nexport const getAttachmentLabel = (data: AttachmentData): string => {\n  if (data.type === \"source-document\") {\n    return data.title || data.filename || \"Source\";\n  }\n\n  const category = getMediaCategory(data);\n  return data.filename || (category === \"image\" ? \"Image\" : \"Attachment\");\n};\n\nconst renderAttachmentImage = (\n  url: string,\n  filename: string | undefined,\n  isGrid: boolean\n) =>\n  isGrid ? (\n    <img\n      alt={filename || \"Image\"}\n      className=\"size-full object-cover\"\n      height={96}\n      src={url}\n      width={96}\n    />\n  ) : (\n    <img\n      alt={filename || \"Image\"}\n      className=\"size-full rounded object-cover\"\n      height={20}\n      src={url}\n      width={20}\n    />\n  );\n\n// ============================================================================\n// Contexts\n// ============================================================================\n\ninterface AttachmentsContextValue {\n  variant: AttachmentVariant;\n}\n\nconst AttachmentsContext = createContext<AttachmentsContextValue | null>(null);\n\ninterface AttachmentContextValue {\n  data: AttachmentData;\n  mediaCategory: AttachmentMediaCategory;\n  onRemove?: () => void;\n  variant: AttachmentVariant;\n}\n\nconst AttachmentContext = createContext<AttachmentContextValue | null>(null);\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport const useAttachmentsContext = () =>\n  useContext(AttachmentsContext) ?? { variant: \"grid\" as const };\n\nexport const useAttachmentContext = () => {\n  const ctx = useContext(AttachmentContext);\n  if (!ctx) {\n    throw new Error(\"Attachment components must be used within <Attachment>\");\n  }\n  return ctx;\n};\n\n// ============================================================================\n// Attachments - Container\n// ============================================================================\n\nexport type AttachmentsProps = HTMLAttributes<HTMLDivElement> & {\n  variant?: AttachmentVariant;\n};\n\nexport const Attachments = ({\n  variant = \"grid\",\n  className,\n  children,\n  ...props\n}: AttachmentsProps) => {\n  const contextValue = useMemo(() => ({ variant }), [variant]);\n\n  return (\n    <AttachmentsContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          \"flex items-start\",\n          variant === \"list\" ? \"flex-col gap-2\" : \"flex-wrap gap-2\",\n          variant === \"grid\" && \"ml-auto w-fit\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </AttachmentsContext.Provider>\n  );\n};\n\n// ============================================================================\n// Attachment - Item\n// ============================================================================\n\nexport type AttachmentProps = HTMLAttributes<HTMLDivElement> & {\n  data: AttachmentData;\n  onRemove?: () => void;\n};\n\nexport const Attachment = ({\n  data,\n  onRemove,\n  className,\n  children,\n  ...props\n}: AttachmentProps) => {\n  const { variant } = useAttachmentsContext();\n  const mediaCategory = getMediaCategory(data);\n\n  const contextValue = useMemo<AttachmentContextValue>(\n    () => ({ data, mediaCategory, onRemove, variant }),\n    [data, mediaCategory, onRemove, variant]\n  );\n\n  return (\n    <AttachmentContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          \"group relative\",\n          variant === \"grid\" && \"size-24 overflow-hidden rounded-lg\",\n          variant === \"inline\" && [\n            \"flex h-8 cursor-pointer select-none items-center gap-1.5\",\n            \"rounded-md border border-border px-1.5\",\n            \"font-medium text-sm transition-all\",\n            \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n          ],\n          variant === \"list\" && [\n            \"flex w-full items-center gap-3 rounded-lg border p-3\",\n            \"hover:bg-accent/50\",\n          ],\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </AttachmentContext.Provider>\n  );\n};\n\n// ============================================================================\n// AttachmentPreview - Media preview\n// ============================================================================\n\nexport type AttachmentPreviewProps = HTMLAttributes<HTMLDivElement> & {\n  fallbackIcon?: ReactNode;\n};\n\nexport const AttachmentPreview = ({\n  fallbackIcon,\n  className,\n  ...props\n}: AttachmentPreviewProps) => {\n  const { data, mediaCategory, variant } = useAttachmentContext();\n\n  const iconSize = variant === \"inline\" ? \"size-3\" : \"size-4\";\n\n  const renderIcon = (Icon: typeof ImageIcon) => (\n    <Icon className={cn(iconSize, \"text-muted-foreground\")} />\n  );\n\n  const renderContent = () => {\n    if (mediaCategory === \"image\" && data.type === \"file\" && data.url) {\n      return renderAttachmentImage(data.url, data.filename, variant === \"grid\");\n    }\n\n    if (mediaCategory === \"video\" && data.type === \"file\" && data.url) {\n      return <video className=\"size-full object-cover\" muted src={data.url} />;\n    }\n\n    const Icon = mediaCategoryIcons[mediaCategory];\n    return fallbackIcon ?? renderIcon(Icon);\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex shrink-0 items-center justify-center overflow-hidden\",\n        variant === \"grid\" && \"size-full bg-muted\",\n        variant === \"inline\" && \"size-5 rounded bg-background\",\n        variant === \"list\" && \"size-12 rounded bg-muted\",\n        className\n      )}\n      {...props}\n    >\n      {renderContent()}\n    </div>\n  );\n};\n\n// ============================================================================\n// AttachmentInfo - Name and type display\n// ============================================================================\n\nexport type AttachmentInfoProps = HTMLAttributes<HTMLDivElement> & {\n  showMediaType?: boolean;\n};\n\nexport const AttachmentInfo = ({\n  showMediaType = false,\n  className,\n  ...props\n}: AttachmentInfoProps) => {\n  const { data, variant } = useAttachmentContext();\n  const label = getAttachmentLabel(data);\n\n  if (variant === \"grid\") {\n    return null;\n  }\n\n  return (\n    <div className={cn(\"min-w-0 flex-1\", className)} {...props}>\n      <span className=\"block truncate\">{label}</span>\n      {showMediaType && data.mediaType && (\n        <span className=\"block truncate text-muted-foreground text-xs\">\n          {data.mediaType}\n        </span>\n      )}\n    </div>\n  );\n};\n\n// ============================================================================\n// AttachmentRemove - Remove button\n// ============================================================================\n\nexport type AttachmentRemoveProps = ComponentProps<typeof Button> & {\n  label?: string;\n};\n\nexport const AttachmentRemove = ({\n  label = \"Remove\",\n  className,\n  children,\n  ...props\n}: AttachmentRemoveProps) => {\n  const { onRemove, variant } = useAttachmentContext();\n\n  const handleClick = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      onRemove?.();\n    },\n    [onRemove]\n  );\n\n  if (!onRemove) {\n    return null;\n  }\n\n  return (\n    <Button\n      aria-label={label}\n      className={cn(\n        variant === \"grid\" && [\n          \"absolute top-2 right-2 size-6 rounded-full p-0\",\n          \"bg-background/80 backdrop-blur-sm\",\n          \"opacity-0 transition-opacity group-hover:opacity-100\",\n          \"hover:bg-background\",\n          \"[&>svg]:size-3\",\n        ],\n        variant === \"inline\" && [\n          \"size-5 rounded p-0\",\n          \"opacity-0 transition-opacity group-hover:opacity-100\",\n          \"[&>svg]:size-2.5\",\n        ],\n        variant === \"list\" && [\"size-8 shrink-0 rounded p-0\", \"[&>svg]:size-4\"],\n        className\n      )}\n      onClick={handleClick}\n      type=\"button\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <XIcon />}\n      <span className=\"sr-only\">{label}</span>\n    </Button>\n  );\n};\n\n// ============================================================================\n// AttachmentHoverCard - Hover preview\n// ============================================================================\n\nexport type AttachmentHoverCardProps = ComponentProps<typeof HoverCard>;\n\nexport const AttachmentHoverCard = ({\n  openDelay = 0,\n  closeDelay = 0,\n  ...props\n}: AttachmentHoverCardProps) => (\n  <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />\n);\n\nexport type AttachmentHoverCardTriggerProps = ComponentProps<\n  typeof HoverCardTrigger\n>;\n\nexport const AttachmentHoverCardTrigger = (\n  props: AttachmentHoverCardTriggerProps\n) => <HoverCardTrigger {...props} />;\n\nexport type AttachmentHoverCardContentProps = ComponentProps<\n  typeof HoverCardContent\n>;\n\nexport const AttachmentHoverCardContent = ({\n  align = \"start\",\n  className,\n  ...props\n}: AttachmentHoverCardContentProps) => (\n  <HoverCardContent\n    align={align}\n    className={cn(\"w-auto p-2\", className)}\n    {...props}\n  />\n);\n\n// ============================================================================\n// AttachmentEmpty - Empty state\n// ============================================================================\n\nexport type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;\n\nexport const AttachmentEmpty = ({\n  className,\n  children,\n  ...props\n}: AttachmentEmptyProps) => (\n  <div\n    className={cn(\n      \"flex items-center justify-center p-4 text-muted-foreground text-sm\",\n      className\n    )}\n    {...props}\n  >\n    {children ?? \"No attachments\"}\n  </div>\n);\n",
      "type": "registry:ui",
      "target": "components/ui/attachments.tsx"
    },
    {
      "path": "components/ui/prompt-input.tsx",
      "content": "\"use client\";\n\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\";\nimport { cn } from \"@/lib/utils\";\nimport type { ChatStatus, FileUIPart, SourceDocumentUIPart } from \"ai\";\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  PropsWithChildren,\n  ReactNode,\n  RefObject,\n} from \"react\";\nimport {\n  Children,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\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    // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\n    return new Promise((resolve) => {\n      const reader = new FileReader();\n      // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n      reader.onloadend = () => resolve(reader.result as string);\n      // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\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    // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\n    await new Promise<void>((resolve, reject) => {\n      // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n      video.onloadedmetadata = () => resolve();\n      // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\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    // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\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\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\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 = () =>\n  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 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  // oxlint-disable-next-line eslint(no-empty-function)\n  const openRef = useRef<() => void>(() => {});\n\n  const add = useCallback((files: File[] | FileList) => {\n    const incoming = [...files];\n    if (incoming.length === 0) {\n      return;\n    }\n\n    setAttachmentFiles((prev) => [\n      ...prev,\n      ...incoming.map((file) => ({\n        filename: file.name,\n        id: nanoid(),\n        mediaType: file.type,\n        type: \"file\" as const,\n        url: URL.createObjectURL(file),\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\n  useEffect(() => {\n    attachmentsRef.current = attachmentFiles;\n  }, [attachmentFiles]);\n\n  // Cleanup blob URLs on unmount to prevent memory leaks\n  useEffect(\n    () => () => {\n      for (const f of attachmentsRef.current) {\n        if (f.url) {\n          URL.revokeObjectURL(f.url);\n        }\n      }\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: setTextInput,\n        value: textInput,\n      },\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  // 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\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\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  const handleSelect = useCallback(\n    (e: Event) => {\n      e.preventDefault();\n      attachments.openFileDialog();\n    },\n    [attachments]\n  );\n\n  return (\n    <DropdownMenuItem {...props} onSelect={handleSelect}>\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  onSelect,\n  ...props\n}: PromptInputActionAddScreenshotProps) => {\n  const attachments = usePromptInputAttachments();\n\n  const handleSelect = useCallback(\n    async (event: Event) => {\n      onSelect?.(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        throw error;\n      }\n    },\n    [onSelect, attachments]\n  );\n\n  return (\n    <DropdownMenuItem {...props} onSelect={handleSelect}>\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  // 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  // bytes\n  maxFileSize?: number;\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  // ----- Local referenced sources (always local to PromptInput)\n  const [referencedSources, setReferencedSources] = useState<\n    (SourceDocumentUIPart & { id: string })[]\n  >([]);\n\n  // Keep a ref to files for cleanup on unmount (avoids stale closure)\n  const filesRef = useRef(files);\n\n  useEffect(() => {\n    filesRef.current = files;\n  }, [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          // e.g: image/* -> image/\n          const prefix = pattern.slice(0, -1);\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 = [...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            filename: file.name,\n            id: nanoid(),\n            mediaType: file.type,\n            type: \"file\",\n            url: URL.createObjectURL(file),\n          });\n        }\n        return [...prev, ...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  // Wrapper that validates files before calling provider's add\n  const addWithProviderValidation = useCallback(\n    (fileList: File[] | FileList) => {\n      const incoming = [...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      const currentCount = files.length;\n      const capacity =\n        typeof maxFiles === \"number\"\n          ? Math.max(0, maxFiles - currentCount)\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\n      if (capped.length > 0) {\n        controller?.attachments.add(capped);\n      }\n    },\n    [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller]\n  );\n\n  const clearAttachments = useCallback(\n    () =>\n      usingProvider\n        ? controller?.attachments.clear()\n        : setItems((prev) => {\n            for (const file of prev) {\n              if (file.url) {\n                URL.revokeObjectURL(file.url);\n              }\n            }\n            return [];\n          }),\n    [usingProvider, controller]\n  );\n\n  const clearReferencedSources = useCallback(\n    () => setReferencedSources([]),\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  const clear = useCallback(() => {\n    clearAttachments();\n    clearReferencedSources();\n  }, [clearAttachments, clearReferencedSources]);\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  // 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) {\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        for (const f of filesRef.current) {\n          if (f.url) {\n            URL.revokeObjectURL(f.url);\n          }\n        }\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        ? 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      try {\n        // Convert blob URLs to data URLs asynchronously\n        const convertedFiles: FileUIPart[] = await Promise.all(\n          files.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        const result = onSubmit({ files: convertedFiles, text }, event);\n\n        // Handle both sync and async onSubmit\n        if (result instanceof Promise) {\n          try {\n            await result;\n            clear();\n            if (usingProvider) {\n              controller.textInput.clear();\n            }\n          } catch {\n            // Don't clear on error - user may want to retry\n          }\n        } else {\n          // Sync function completed without throwing, clear inputs\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    [usingProvider, controller, files, onSubmit, clear]\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      {inner}\n    </LocalReferencedSourcesContext.Provider>\n  );\n\n  // Always provide LocalAttachmentsContext so children get validated add function\n  return (\n    <LocalAttachmentsContext.Provider value={attachmentsCtx}>\n      {withReferencedSources}\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<\n  typeof InputGroupTextarea\n>;\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 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        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;\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    [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      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\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\n    className={cn(\"flex min-w-0 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\nexport const PromptInputButton = ({\n  variant = \"ghost\",\n  className,\n  size,\n  tooltip,\n  ...props\n}: PromptInputButtonProps) => {\n  const newSize =\n    size ?? (Children.count(props.children) > 1 ? \"sm\" : \"icon-sm\");\n\n  const button = (\n    <InputGroupButton\n      className={cn(className)}\n      size={newSize}\n      type=\"button\"\n      variant={variant}\n      {...props}\n    />\n  );\n\n  if (!tooltip) {\n    return button;\n  }\n\n  const tooltipContent =\n    typeof tooltip === \"string\" ? tooltip : tooltip.content;\n  const shortcut = typeof tooltip === \"string\" ? undefined : tooltip.shortcut;\n  const side = typeof tooltip === \"string\" ? \"top\" : (tooltip.side ?? \"top\");\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent side={side}>\n        {tooltipContent}\n        {shortcut && (\n          <span className=\"ml-2 text-muted-foreground\">{shortcut}</span>\n        )}\n      </TooltipContent>\n    </Tooltip>\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 asChild>\n    <PromptInputButton className={className} {...props}>\n      {children ?? <PlusIcon className=\"size-4\" />}\n    </PromptInputButton>\n  </DropdownMenuTrigger>\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\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\n  let Icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    Icon = <Spinner />;\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  const handleClick = useCallback(\n    (e: React.MouseEvent<HTMLButtonElement>) => {\n      if (isGenerating && onStop) {\n        e.preventDefault();\n        onStop();\n        return;\n      }\n      onClick?.(e);\n    },\n    [isGenerating, onStop, onClick]\n  );\n\n  return (\n    <InputGroupButton\n      aria-label={isGenerating ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      onClick={handleClick}\n      size={size}\n      type={isGenerating && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {children ?? Icon}\n    </InputGroupButton>\n  );\n};\n\nexport type PromptInputSelectProps = ComponentProps<typeof Select>;\n\nexport const PromptInputSelect = (props: PromptInputSelectProps) => (\n  <Select {...props} />\n);\n\nexport type PromptInputSelectTriggerProps = ComponentProps<\n  typeof SelectTrigger\n>;\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<\n  typeof SelectContent\n>;\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\nexport type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;\n\nexport const PromptInputHoverCard = ({\n  openDelay = 0,\n  closeDelay = 0,\n  ...props\n}: PromptInputHoverCardProps) => (\n  <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...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 = ({\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  // oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)\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/ui/prompt-input.tsx"
    }
  ],
  "type": "registry:block"
}