{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-attachment",
  "type": "registry:ui",
  "dependencies": [
    "@tabler/icons-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/agent-elements/components/input/file-attachment.tsx",
      "content": "import { useState } from \"react\";\nimport {\n  IconX as X,\n  IconFileText as FileText,\n  IconFileCode as FileCode,\n  IconFileTypeJs as FileJson,\n  IconPhoto as ImageIcon,\n} from \"@tabler/icons-react\";\nimport { cn } from \"../utils/cn\";\nimport { ImageLightbox } from \"../image-lightbox\";\n\nexport type FileAttachmentProps = {\n  id: string;\n  filename: string;\n  size?: number;\n  isImage?: boolean;\n  url?: string;\n  onRemove?: () => void;\n  className?: string;\n  display?: \"chip\" | \"image-only\";\n  /**\n   * When true (default) clicking the image thumbnail opens a fullscreen\n   * preview. Set to false to render a non-interactive thumbnail.\n   */\n  enableImagePreview?: boolean;\n};\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\ntype FileIconName = \"image\" | \"code\" | \"data\" | \"text\";\n\nfunction getFileIconName(filename: string, isImage?: boolean): FileIconName {\n  if (isImage) return \"image\";\n\n  const ext = filename.split(\".\").pop()?.toLowerCase();\n\n  if (\n    [\n      \"js\",\n      \"ts\",\n      \"jsx\",\n      \"tsx\",\n      \"py\",\n      \"rb\",\n      \"go\",\n      \"rs\",\n      \"java\",\n      \"kt\",\n      \"swift\",\n      \"c\",\n      \"cpp\",\n      \"h\",\n      \"hpp\",\n      \"cs\",\n      \"php\",\n    ].includes(ext || \"\")\n  ) {\n    return \"code\";\n  }\n\n  if ([\"json\", \"yaml\", \"yml\", \"xml\"].includes(ext || \"\")) {\n    return \"data\";\n  }\n\n  return \"text\";\n}\n\nfunction renderFileIcon(iconName: FileIconName) {\n  switch (iconName) {\n    case \"image\":\n      return <ImageIcon className=\"size-4 text-muted-foreground\" />;\n    case \"code\":\n      return <FileCode className=\"size-4 text-muted-foreground\" />;\n    case \"data\":\n      return <FileJson className=\"size-4 text-muted-foreground\" />;\n    default:\n      return <FileText className=\"size-4 text-muted-foreground\" />;\n  }\n}\n\nexport function FileAttachment({\n  id,\n  filename,\n  size,\n  isImage,\n  url,\n  onRemove,\n  className,\n  display = \"chip\",\n  enableImagePreview = true,\n}: FileAttachmentProps) {\n  const [isHovered, setIsHovered] = useState(false);\n  const [isLightboxOpen, setIsLightboxOpen] = useState(false);\n  const iconName = getFileIconName(filename, isImage);\n  const isImageOnly = display === \"image-only\" && isImage && !!url;\n  const canPreview = Boolean(enableImagePreview && isImage && url);\n\n  const openLightbox = (event: React.MouseEvent) => {\n    event.stopPropagation();\n    setIsLightboxOpen(true);\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative bg-muted/50 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding))]\",\n        isImageOnly\n          ? \"size-10 flex items-center justify-center\"\n          : \"flex items-center gap-2 pl-1 pr-2 py-1 min-w-[120px] max-w-[200px]\",\n        className,\n      )}\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n    >\n      {isImageOnly ? (\n        <div\n          className={cn(\n            \"size-8 overflow-hidden shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]\",\n            canPreview && \"cursor-pointer\",\n          )}\n          onClick={canPreview ? openLightbox : undefined}\n        >\n          <img\n            src={url}\n            alt={filename}\n            className=\"w-full h-full object-cover\"\n          />\n        </div>\n      ) : (\n        <>\n          {isImage && url ? (\n            <div\n              className={cn(\n                \"w-8 self-stretch overflow-hidden shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]\",\n                canPreview && \"cursor-pointer\",\n              )}\n              onClick={canPreview ? openLightbox : undefined}\n            >\n              <img\n                src={url}\n                alt={filename}\n                className=\"w-full h-full object-cover aspect-square\"\n              />\n            </div>\n          ) : (\n            <div className=\"flex items-center justify-center w-8 self-stretch bg-muted shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]\">\n              {renderFileIcon(iconName)}\n            </div>\n          )}\n\n          <div className=\"flex flex-col min-w-0\">\n            <span\n              className=\"text-sm font-medium text-foreground truncate\"\n              title={filename}\n            >\n              {filename}\n            </span>\n            {size !== undefined && (\n              <span className=\"text-[10px] text-muted-foreground\">\n                {formatFileSize(size)}\n              </span>\n            )}\n          </div>\n        </>\n      )}\n\n      {onRemove && (\n        <button\n          onClick={(e) => {\n            e.stopPropagation();\n            onRemove();\n          }}\n          className={`absolute -top-1.5 -right-1.5 size-4 rounded-full bg-background border border-border\n                     flex items-center justify-center transition-[opacity,transform] duration-150 ease-out active:scale-[0.97] z-10\n                     text-muted-foreground hover:text-foreground\n                     ${isHovered ? \"opacity-100\" : \"opacity-0\"}`}\n          type=\"button\"\n        >\n          <X className=\"size-3\" />\n        </button>\n      )}\n\n      {canPreview && url && (\n        <ImageLightbox\n          open={isLightboxOpen}\n          onClose={() => setIsLightboxOpen(false)}\n          images={[{ id, url, filename }]}\n        />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/input/file-attachment.tsx"
    },
    {
      "path": "registry/agent-elements/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/cn.ts"
    },
    {
      "path": "registry/agent-elements/components/image-lightbox.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  IconChevronLeft,\n  IconChevronRight,\n  IconX,\n} from \"@tabler/icons-react\";\nimport { cn } from \"./utils/cn\";\n\nexport type LightboxImage = {\n  /** Stable identifier — used for keys and to know which image is active. */\n  id: string;\n  /** Resolvable image URL (https / data: / blob:). */\n  url: string;\n  /** Optional filename used for the alt text. */\n  filename?: string;\n};\n\nexport type ImageLightboxProps = {\n  /** Whether the overlay is open. */\n  open: boolean;\n  /** Close handler — wired to overlay click, X button, and Esc key. */\n  onClose: () => void;\n  /** Full set of images for gallery navigation. */\n  images: LightboxImage[];\n  /** Index in `images` to start on. */\n  initialIndex?: number;\n};\n\n/**\n * Portal-based fullscreen image preview. Renders to `document.body` so it\n * escapes any clipping/transform/stacking context. Adapted from the\n * 21st-private-1 desktop chat — without copy/save (those are\n * desktop-API-specific).\n */\nexport function ImageLightbox({\n  open,\n  onClose,\n  images,\n  initialIndex = 0,\n}: ImageLightboxProps) {\n  const [currentIndex, setCurrentIndex] = useState(initialIndex);\n  const hasMultipleImages = images.length > 1;\n\n  // Sync the active index whenever the consumer re-opens with a new initial.\n  useEffect(() => {\n    if (open) setCurrentIndex(initialIndex);\n  }, [open, initialIndex]);\n\n  const goToPrevious = useCallback(\n    (event?: React.MouseEvent) => {\n      event?.stopPropagation();\n      setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));\n    },\n    [images.length],\n  );\n\n  const goToNext = useCallback(\n    (event?: React.MouseEvent) => {\n      event?.stopPropagation();\n      setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));\n    },\n    [images.length],\n  );\n\n  // Esc / arrow-key navigation. Capture phase so we beat any local handlers\n  // (e.g. an Editor that swallows Esc).\n  useEffect(() => {\n    if (!open) return;\n    const handleKeyDown = (event: KeyboardEvent) => {\n      switch (event.key) {\n        case \"Escape\":\n          event.preventDefault();\n          event.stopPropagation();\n          onClose();\n          break;\n        case \"ArrowLeft\":\n          if (hasMultipleImages) goToPrevious();\n          break;\n        case \"ArrowRight\":\n          if (hasMultipleImages) goToNext();\n          break;\n      }\n    };\n    window.addEventListener(\"keydown\", handleKeyDown, true);\n    return () =>\n      window.removeEventListener(\"keydown\", handleKeyDown, true);\n  }, [open, hasMultipleImages, onClose, goToPrevious, goToNext]);\n\n  // Lock body scroll while open so the page underneath doesn't move.\n  useEffect(() => {\n    if (!open) return;\n    const previousOverflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = previousOverflow;\n    };\n  }, [open]);\n\n  if (typeof document === \"undefined\") return null;\n  if (!open) return null;\n  const currentImage = images[currentIndex] ?? images[0];\n  if (!currentImage?.url) return null;\n\n  return createPortal(\n    <div\n      role=\"dialog\"\n      aria-modal=\"true\"\n      className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm\"\n      onClick={onClose}\n    >\n      <button\n        type=\"button\"\n        onClick={onClose}\n        aria-label=\"Close fullscreen (Esc)\"\n        className=\"absolute top-4 right-4 z-10 inline-flex size-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n      >\n        <IconX className=\"size-5\" />\n      </button>\n\n      {hasMultipleImages && (\n        <button\n          type=\"button\"\n          onClick={goToPrevious}\n          aria-label=\"Previous image (←)\"\n          className=\"absolute left-4 top-1/2 z-10 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n        >\n          <IconChevronLeft className=\"size-6\" />\n        </button>\n      )}\n\n      <img\n        src={currentImage.url}\n        alt={currentImage.filename ?? \"Image preview\"}\n        className=\"max-w-[90vw] max-h-[85vh] object-contain select-none\"\n        onClick={(event) => event.stopPropagation()}\n        draggable={false}\n      />\n\n      {hasMultipleImages && (\n        <button\n          type=\"button\"\n          onClick={goToNext}\n          aria-label=\"Next image (→)\"\n          className=\"absolute right-4 top-1/2 z-10 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n        >\n          <IconChevronRight className=\"size-6\" />\n        </button>\n      )}\n\n      {hasMultipleImages && (\n        <div className=\"absolute bottom-6 left-1/2 -translate-x-1/2 flex flex-col items-center gap-3\">\n          <div className=\"flex gap-2\">\n            {images.map((_, idx) => (\n              <button\n                key={idx}\n                type=\"button\"\n                onClick={(event) => {\n                  event.stopPropagation();\n                  setCurrentIndex(idx);\n                }}\n                aria-label={`Go to image ${idx + 1}`}\n                className={cn(\n                  \"size-2 rounded-full transition-all\",\n                  idx === currentIndex\n                    ? \"bg-white scale-125\"\n                    : \"bg-white/40 hover:bg-white/60\",\n                )}\n              />\n            ))}\n          </div>\n          <span className=\"text-white/70 text-sm\">\n            {currentIndex + 1} / {images.length}\n          </span>\n        </div>\n      )}\n    </div>,\n    document.body,\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/image-lightbox.tsx"
    },
    {
      "path": "registry/agent-elements/agent-ui.css",
      "content": "/* Agent Elements tokens + utilities */\n\n/*\n * AN Agent Chat - CSS Custom Properties\n * These are the default values. Override by redefining these vars in your app CSS.\n */\n:root {\n  /* Geometry — all radii derive from --an-border-radius */\n  --an-border-radius: 16px;\n  --an-message-border-radius: var(--an-border-radius);\n  --an-input-border-radius: var(--an-border-radius);\n  --an-tool-border-radius: 10px;\n  --an-message-radius-inner-offset: 4px;\n  --an-max-width: 420px;\n\n  /* Colors - Light mode defaults */\n  --an-background: #ffffff;\n  --an-background-secondary: #f0f0f0;\n  --an-background-tertiary: #f8f8f8;\n  --an-foreground: #1a1a1a;\n  --an-foreground-muted: #737373;\n  --an-foreground-subtle: #a3a3a3;\n  --an-border-color: #e4e4e7;\n  --an-primary-color: #3b82f6;\n\n  /* User Messages */\n  --an-user-message-bg: #f5f5f5;\n  --an-user-message-text: #1a1a1a;\n\n  /* Input */\n  --an-input-background: #ffffff;\n  --an-input-border-color: #e4e4e7;\n  --an-input-color: #1a1a1a;\n  --an-input-placeholder-color: #a3a3a3;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 10px;\n\n  /* Send/Stop Buttons */\n  --an-send-button-bg: #3b82f6;\n  --an-send-button-color: #ffffff;\n\n  /* Tools */\n  --an-tool-background: #f5f5f5;\n  --an-tool-border-color: #e4e4e7;\n  --an-tool-color: #1a1a1a;\n  --an-tool-color-muted: #737373;\n\n  /* Code */\n  --an-code-background: #1e1e1e;\n  --an-code-color: #d4d4d4;\n\n  /* Diff colors */\n  --an-diff-added-bg: rgba(34, 197, 94, 0.1);\n  --an-diff-added-border: rgba(34, 197, 94, 0.5);\n  --an-diff-added-text: #15803d;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.1);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.5);\n  --an-diff-removed-text: #dc2626;\n}\n\n/* Dark mode defaults */\n.dark {\n  --an-background: #0a0a0a;\n  --an-background-secondary: #242424;\n  --an-background-tertiary: #141414;\n  --an-foreground: #fafafa;\n  --an-foreground-muted: #8c8c8c;\n  --an-foreground-subtle: #71717a;\n  --an-border-color: #2a2a2a;\n  --an-primary-color: #60a5fa;\n\n  --an-user-message-bg: #1a1a1a;\n  --an-user-message-text: #fafafa;\n\n  --an-input-background: #0a0a0a;\n  --an-input-border-color: #2a2a2a;\n  --an-input-color: #fafafa;\n  --an-input-placeholder-color: #71717a;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 12px;\n\n  --an-send-button-bg: #60a5fa;\n  /* Dark mode uses a lighter primary (#60a5fa) — pair it with black text so\n     labels like \"Approve\" / send-arrow have proper contrast instead of\n     low-contrast white-on-light-blue. */\n  --an-send-button-color: #0a0a0a;\n\n  --an-tool-background: #141414;\n  --an-tool-border-color: #2a2a2a;\n  --an-tool-color: #fafafa;\n  --an-tool-color-muted: #8c8c8c;\n\n  --an-code-background: #0a0a0a;\n  --an-code-color: #d4d4d4;\n\n  --an-diff-added-bg: rgba(34, 197, 94, 0.15);\n  --an-diff-added-border: rgba(34, 197, 94, 0.4);\n  --an-diff-added-text: #4ade80;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.15);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.4);\n  --an-diff-removed-text: #f87171;\n}\n\n@theme inline {\n  --color-an-background: var(--an-background);\n  --color-an-background-secondary: var(--an-background-secondary);\n  --color-an-background-tertiary: var(--an-background-tertiary);\n  --color-an-foreground: var(--an-foreground);\n  --color-an-foreground-muted: var(--an-foreground-muted);\n  --color-an-foreground-subtle: var(--an-foreground-subtle);\n  --color-an-border-color: var(--an-border-color);\n  --color-an-primary-color: var(--an-primary-color);\n  --color-an-user-message-bg: var(--an-user-message-bg);\n  --color-an-user-message-text: var(--an-user-message-text);\n  --color-an-input-background: var(--an-input-background);\n  --color-an-input-border-color: var(--an-input-border-color);\n  --color-an-input-color: var(--an-input-color);\n  --color-an-input-placeholder-color: var(--an-input-placeholder-color);\n  --color-an-input-focus-outline: var(--an-input-focus-outline);\n  --color-an-send-button-bg: var(--an-send-button-bg);\n  --color-an-send-button-color: var(--an-send-button-color);\n  --color-an-tool-background: var(--an-tool-background);\n  --color-an-tool-border-color: var(--an-tool-border-color);\n  --color-an-tool-color: var(--an-tool-color);\n  --color-an-tool-color-muted: var(--an-tool-color-muted);\n  --color-an-code-background: var(--an-code-background);\n  --color-an-code-color: var(--an-code-color);\n  --color-an-diff-added-bg: var(--an-diff-added-bg);\n  --color-an-diff-added-border: var(--an-diff-added-border);\n  --color-an-diff-added-text: var(--an-diff-added-text);\n  --color-an-diff-removed-bg: var(--an-diff-removed-bg);\n  --color-an-diff-removed-border: var(--an-diff-removed-border);\n  --color-an-diff-removed-text: var(--an-diff-removed-text);\n  --radius-an-message: var(--an-message-border-radius);\n  --radius-an-message-inner: calc(\n    var(--an-message-border-radius) - var(--an-message-radius-inner-offset)\n  );\n  --radius-an-input-border-radius: var(--an-input-border-radius);\n  --radius-an-tool-border-radius: var(--an-tool-border-radius);\n  --spacing-an-context-padding: var(--an-context-padding);\n  --max-width-an: var(--an-max-width);\n  --spacing-an-user-message-x: 14px;\n  --spacing-an-user-message-y: 10px;\n}\n\n/* TextShimmer animation */\n@keyframes an-shimmer {\n  from {\n    background-position: 100% center;\n  }\n  to {\n    background-position: 0% center;\n  }\n}\n\n@keyframes an-blink {\n  50% {\n    opacity: 0;\n  }\n}\n\n@keyframes loading-dots {\n  0%,\n  100% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes an-ellipsis {\n  0% {\n    width: 0;\n  }\n  33% {\n    width: 0.33em;\n  }\n  66% {\n    width: 0.66em;\n  }\n  100% {\n    width: 1em;\n  }\n}\n\n.an-text-shimmer {\n  display: inline-block;\n  background-size: 250% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  color: transparent;\n  background-image: linear-gradient(\n    90deg,\n    var(--an-foreground-subtle, #a3a3a3) 0%,\n    var(--an-foreground-subtle, #a3a3a3) 40%,\n    var(--an-foreground-muted, #737373) 50%,\n    var(--an-foreground-subtle, #a3a3a3) 60%,\n    var(--an-foreground-subtle, #a3a3a3) 100%\n  );\n  background-repeat: no-repeat;\n}\n\n.an-text-shimmer--active {\n  animation: an-shimmer var(--an-shimmer-duration, 2s) linear infinite;\n}\n\n.an-ellipsis {\n  display: inline-block;\n  overflow: hidden;\n  width: 0;\n  vertical-align: bottom;\n  animation: an-ellipsis 1.2s steps(1, end) infinite;\n}\n\n.an-markdown pre code {\n  counter-reset: none !important;\n}\n\n.an-markdown pre code > span::before {\n  content: none !important;\n  display: none !important;\n}\n\n.an-markdown pre code > span {\n  padding-left: 0 !important;\n}\n\n/* Diff */\n.an-edit-diff,\n.an-edit-diff pre,\n.an-edit-diff code {\n  font-size: 12px;\n}\n\n.dark .an-edit-tool-card {\n  background-color: #000;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card {\n  background-color: #000;\n}\n\n.dark .an-edit-tool-card .an-edit-diff,\n.dark .an-edit-tool-card .an-edit-diff pre,\n.dark .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff pre,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n/* Markdown Code Block */\n.an-markdown [data-streamdown=\"code-block\"] {\n  padding: 0;\n  border: 1px solid var(--an-border-color);\n  border-radius: var(--an-tool-border-radius);\n  background: transparent;\n  gap: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-header\"] {\n  padding: 0px 10px 0px 8px;\n  height: auto;\n  font-size: 12px;\n  height: 28px;\n  background: transparent;\n  border-bottom: 1px solid var(--color-an-tool-border-color);\n  background-color: var(--an-tool-background);\n  position: relative;\n}\n\n.an-markdown div:has(> [data-streamdown=\"code-block-actions\"]) {\n  position: absolute;\n  height: auto;\n  margin: 0;\n  top: 0;\n  right: 0;\n  height: 28px;\n  padding-right: 8px;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] {\n  background: transparent;\n  border: none;\n  backdrop-filter: none;\n  padding: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] button > svg {\n  width: 14px;\n  height: 14px;\n}\n\n.an-markdown [data-streamdown=\"code-block-body\"] {\n  margin: 0;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  background: transparent;\n  border: transparent;\n  overflow-x: auto;\n}\n\n.an-markdown [data-streamdown=\"code-block\"] pre,\n.an-markdown [data-streamdown=\"code-block\"] code {\n  font-size: 12px;\n  background: transparent;\n}\n",
      "type": "registry:style",
      "target": "components/agent-elements/agent-ui.css"
    }
  ],
  "css": {
    "@import \"../components/agent-elements/agent-ui.css\"": ""
  }
}