{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-attachments",
  "title": "AI: Attachments",
  "description": "File and source attachments with removable chips, list/grid layouts and accessible previews.",
  "dependencies": [
    "@untitledui/icons@^0.0.23"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/hover-card",
    "@nx-ui/ai-types"
  ],
  "files": [
    {
      "path": "components/ai/attachments.tsx",
      "content": "// Ported from vercel/ai-elements@6a9d5b1822ffb10bba4bd97175f01edd7d8651cd.\n\"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 \"./types\";\nimport {\n  File02 as FileTextIcon,\n  Globe01 as GlobeIcon,\n  Image01 as ImageIcon,\n  MusicNote02 as Music2Icon,\n  Paperclip as PaperclipIcon,\n  VideoRecorder as VideoIcon,\n  XClose as XIcon,\n} from \"@untitledui/icons\";\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 group-focus-within:opacity-100 focus: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 group-focus-within:opacity-100 focus: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\nconst AttachmentHoverDelayContext = createContext({ openDelay: 0, closeDelay: 0 });\n\nexport type AttachmentHoverCardProps = ComponentProps<typeof HoverCard> & {\n  openDelay?: number;\n  closeDelay?: number;\n};\n\nexport const AttachmentHoverCard = ({\n  openDelay = 0,\n  closeDelay = 0,\n  ...props\n}: AttachmentHoverCardProps) => (\n  <AttachmentHoverDelayContext.Provider value={{ openDelay, closeDelay }}>\n    <HoverCard {...props} />\n  </AttachmentHoverDelayContext.Provider>\n);\n\nexport type AttachmentHoverCardTriggerProps = ComponentProps<\n  typeof HoverCardTrigger\n>;\n\nexport const AttachmentHoverCardTrigger = (\n  props: AttachmentHoverCardTriggerProps\n) => {\n  const { openDelay, closeDelay } = useContext(AttachmentHoverDelayContext);\n  return <HoverCardTrigger delay={openDelay} closeDelay={closeDelay} {...props} />;\n};\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/ai/attachments.tsx"
    }
  ],
  "type": "registry:ui"
}