{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "type": "registry:component",
  "title": "Attachment",
  "description": "Attach files from the composer and view them inside messages.",
  "registryDependencies": [
    "dialog",
    "tooltip",
    "avatar",
    "https://r.assistant-ui.com/tooltip-icon-button.json"
  ],
  "dependencies": [
    "@assistant-ui/react",
    "lucide-react",
    "zustand"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/attachment.tsx",
      "content": "\"use client\";\n\nimport { type PropsWithChildren, useEffect, useState, type FC } from \"react\";\nimport {\n  XIcon,\n  PlusIcon,\n  FileText,\n  Loader2Icon,\n  AlertCircleIcon,\n} from \"lucide-react\";\nimport {\n  AttachmentPrimitive,\n  ComposerPrimitive,\n  MessagePrimitive,\n  useAuiState,\n  useAui,\n} from \"@assistant-ui/react\";\nimport { useShallow } from \"zustand/shallow\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport {\n  Dialog,\n  DialogTitle,\n  DialogContent,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Avatar,\n  AvatarImage,\n  AvatarFallback,\n} from \"@/components/ui/avatar\";\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\nimport { cn } from \"@/lib/utils\";\n\nconst useFileSrc = (file: File | undefined) => {\n  const [src, setSrc] = useState<string | undefined>(undefined);\n\n  useEffect(() => {\n    if (!file) {\n      setSrc(undefined);\n      return;\n    }\n\n    const objectUrl = URL.createObjectURL(file);\n    setSrc(objectUrl);\n\n    return () => {\n      URL.revokeObjectURL(objectUrl);\n    };\n  }, [file]);\n\n  return src;\n};\n\nconst useAttachmentSrc = () => {\n  const { file, src } = useAuiState(\n    useShallow((s): { file?: File; src?: string } => {\n      if (s.attachment.type !== \"image\") return {};\n      if (s.attachment.file) return { file: s.attachment.file };\n      const src = s.attachment.content?.filter((c) => c.type === \"image\")[0]\n        ?.image;\n      if (!src) return {};\n      return { src };\n    }),\n  );\n\n  return useFileSrc(file) ?? src;\n};\n\ntype AttachmentPreviewProps = {\n  src: string;\n};\n\nconst AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {\n  const [isLoaded, setIsLoaded] = useState(false);\n  return (\n    <img\n      src={src}\n      alt=\"Attachment preview\"\n      className={cn(\n        \"block h-auto max-h-[80vh] w-auto max-w-full rounded-sm object-contain transition-opacity duration-300 motion-reduce:transition-none\",\n        isLoaded\n          ? \"aui-attachment-preview-image-loaded opacity-100\"\n          : \"aui-attachment-preview-image-loading opacity-0\",\n      )}\n      onLoad={() => setIsLoaded(true)}\n    />\n  );\n};\n\nconst AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {\n  const src = useAttachmentSrc();\n\n  if (!src) return children;\n\n  return (\n    <Dialog>\n      <DialogTrigger\n        className=\"aui-attachment-preview-trigger cursor-zoom-in\"\n        asChild\n      >\n        {children}\n      </DialogTrigger>\n      <DialogContent className=\"aui-attachment-preview-dialog-content [&>button]:bg-foreground/60 [&>button]:hover:bg-foreground/80 [&_svg]:text-background p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0!\">\n        <DialogTitle className=\"aui-sr-only sr-only\">\n          Image Attachment Preview\n        </DialogTitle>\n        <div className=\"aui-attachment-preview bg-background relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden rounded-sm\">\n          <AttachmentPreview src={src} />\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst AttachmentThumb: FC = () => {\n  const src = useAttachmentSrc();\n\n  return (\n    <Avatar className=\"aui-attachment-tile-avatar h-full w-full rounded-none\">\n      <AvatarImage\n        src={src}\n        alt=\"Attachment preview\"\n        className=\"aui-attachment-tile-image object-cover\"\n      />\n      <AvatarFallback>\n        <FileText className=\"aui-attachment-tile-fallback-icon text-muted-foreground/80 size-6 stroke-[1.5]\" />\n      </AvatarFallback>\n    </Avatar>\n  );\n};\n\nconst AttachmentUI: FC = () => {\n  const aui = useAui();\n  const isComposer = aui.attachment.source !== \"message\";\n\n  const isImage = useAuiState((s) => s.attachment.type === \"image\");\n  const typeLabel = useAuiState((s) => {\n    const type = s.attachment.type;\n    switch (type) {\n      case \"image\":\n        return \"Image\";\n      case \"document\":\n        return \"Document\";\n      case \"file\":\n        return \"File\";\n      default:\n        return type;\n    }\n  });\n\n  const uploadState = useAuiState((s) =>\n    s.attachment.status.type === \"running\"\n      ? \"uploading\"\n      : s.attachment.status.type === \"incomplete\" &&\n          s.attachment.status.reason === \"error\"\n        ? \"error\"\n        : undefined,\n  );\n  const isUploading = uploadState === \"uploading\";\n  const isError = uploadState === \"error\";\n\n  const errorMessage = useAuiState((s) =>\n    s.attachment.status.type === \"incomplete\" &&\n    s.attachment.status.reason === \"error\"\n      ? (s.attachment.status.message ?? \"Upload failed\")\n      : undefined,\n  );\n\n  return (\n    <Tooltip>\n      <AttachmentPrimitive.Root\n        className={cn(\n          \"aui-attachment-root relative\",\n          isComposer &&\n            \"animate-in fade-in-0 zoom-in-95 duration-200 motion-reduce:animate-none\",\n          isImage &&\n            !isComposer &&\n            \"aui-attachment-root-message only:*:first:size-24\",\n        )}\n      >\n        <AttachmentPreviewDialog>\n          <TooltipTrigger asChild>\n            <div\n              className={cn(\n                \"aui-attachment-tile bg-muted hover:after:bg-foreground/10 focus-visible:ring-ring/50 relative size-14 cursor-pointer overflow-hidden rounded-[calc(var(--composer-radius)-var(--composer-padding))] transition-transform outline-none after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:ring-1 after:ring-black/10 after:transition-colors after:ring-inset focus-visible:ring-3 active:scale-[0.96] motion-reduce:transition-none dark:after:ring-white/10\",\n                isError &&\n                  \"after:ring-destructive/60 dark:after:ring-destructive/60\",\n              )}\n              role=\"button\"\n              tabIndex={0}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  e.preventDefault();\n                  e.currentTarget.click();\n                } else if (e.key === \" \") {\n                  e.preventDefault();\n                }\n              }}\n              onKeyUp={(e) => {\n                if (e.key === \" \") e.currentTarget.click();\n              }}\n              aria-label={`${typeLabel} attachment${\n                isError ? \", upload failed\" : isUploading ? \", uploading\" : \"\"\n              }`}\n            >\n              <AttachmentThumb />\n              {isUploading && (\n                <div\n                  aria-hidden=\"true\"\n                  className=\"aui-attachment-tile-uploading bg-background/60 animate-in fade-in-0 absolute inset-0 flex items-center justify-center backdrop-blur-[2px] motion-reduce:animate-none\"\n                >\n                  <Loader2Icon className=\"text-muted-foreground size-4 animate-spin\" />\n                </div>\n              )}\n              {isError && (\n                <div\n                  aria-hidden=\"true\"\n                  className=\"aui-attachment-tile-error bg-background/70 animate-in fade-in-0 absolute inset-0 flex items-center justify-center backdrop-blur-[2px] motion-reduce:animate-none\"\n                >\n                  <AlertCircleIcon className=\"text-destructive size-4\" />\n                </div>\n              )}\n            </div>\n          </TooltipTrigger>\n        </AttachmentPreviewDialog>\n        {isComposer && <AttachmentRemove />}\n      </AttachmentPrimitive.Root>\n      <TooltipContent side=\"top\">\n        <AttachmentPrimitive.Name />\n        {errorMessage && (\n          <p className=\"aui-attachment-error-message\">{errorMessage}</p>\n        )}\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n\nconst AttachmentRemove: FC = () => {\n  return (\n    <AttachmentPrimitive.Remove asChild>\n      <TooltipIconButton\n        tooltip=\"Remove file\"\n        className=\"aui-attachment-tile-remove absolute end-1 top-1 size-5 rounded-full bg-black/50! text-white backdrop-blur-sm after:absolute after:-inset-1.5 hover:bg-black/70! hover:text-white! active:scale-[0.96] motion-reduce:transition-none\"\n        side=\"top\"\n      >\n        <XIcon className=\"aui-attachment-remove-icon size-3 stroke-[2.5]\" />\n      </TooltipIconButton>\n    </AttachmentPrimitive.Remove>\n  );\n};\n\nexport const UserMessageAttachments: FC = () => {\n  return (\n    <div className=\"aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2\">\n      <MessagePrimitive.Attachments>\n        {() => <AttachmentUI />}\n      </MessagePrimitive.Attachments>\n    </div>\n  );\n};\n\nexport const ComposerAttachments: FC = () => {\n  return (\n    <div className=\"aui-composer-attachments flex w-full flex-row items-center gap-2 overflow-x-auto empty:hidden\">\n      <ComposerPrimitive.Attachments>\n        {() => <AttachmentUI />}\n      </ComposerPrimitive.Attachments>\n    </div>\n  );\n};\n\nexport const ComposerAddAttachment: FC = () => {\n  return (\n    <ComposerPrimitive.AddAttachment asChild>\n      <TooltipIconButton\n        tooltip=\"Add Attachment\"\n        side=\"bottom\"\n        variant=\"ghost\"\n        size=\"icon\"\n        className=\"aui-composer-add-attachment hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30 size-7 rounded-full p-1 text-xs font-semibold active:scale-[0.96] motion-reduce:transition-none\"\n        aria-label=\"Add Attachment\"\n      >\n        <PlusIcon className=\"aui-attachment-add-icon size-4.5 stroke-[1.5px]\" />\n      </TooltipIconButton>\n    </ComposerPrimitive.AddAttachment>\n  );\n};\n"
    }
  ]
}