{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "title": "Attachment",
  "description": "A file with type icon or thumbnail, upload progress, failure and retry, and removal.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "progress",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/attachment.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  CircleAlert,\n  File,\n  FileArchive,\n  FileAudio,\n  FileCode,\n  FileSpreadsheet,\n  FileText,\n  FileVideo,\n  ImageIcon,\n  type LucideIcon,\n  RotateCw,\n  X,\n} from \"lucide-react\";\nimport { type ComponentProps, createContext, useContext, useId } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const ATTACHMENT_VARIANTS = [\"row\", \"card\", \"chip\"] as const;\nexport type AttachmentVariant = (typeof ATTACHMENT_VARIANTS)[number];\nexport type AttachmentStatus = \"uploading\" | \"ready\" | \"error\";\nexport type AttachmentProps = ComponentProps<\"div\"> & {\n  variant?: AttachmentVariant;\n  status?: AttachmentStatus;\n  /** Upload progress from 0 to 100. Omit for indeterminate progress. */\n  progress?: number;\n  /** MIME type, used to choose the default file icon. */\n  mimeType?: string;\n};\n\ntype AttachmentContextValue = {\n  id: string;\n  variant: AttachmentVariant;\n  status: AttachmentStatus;\n  progress?: number;\n  mimeType: string;\n};\n\nconst AttachmentContext = createContext<AttachmentContextValue | null>(null);\n\nfunction useAttachment(part: string) {\n  const context = useContext(AttachmentContext);\n  if (!context) throw new Error(`${part} must be used within Attachment`);\n  return context;\n}\n\nfunction iconFor(mimeType: string): LucideIcon {\n  if (mimeType.startsWith(\"image/\")) return ImageIcon;\n  if (mimeType.startsWith(\"video/\")) return FileVideo;\n  if (mimeType.startsWith(\"audio/\")) return FileAudio;\n  if (/zip|tar|gzip|compressed/.test(mimeType)) return FileArchive;\n  if (/csv|spreadsheet|excel/.test(mimeType)) return FileSpreadsheet;\n  if (/json|javascript|typescript|xml|html|x-/.test(mimeType)) return FileCode;\n  if (mimeType.startsWith(\"text/\") || /pdf|document|word/.test(mimeType)) return FileText;\n  return File;\n}\n\nconst dangerText = \"text-destructive-ink\";\n\nconst attachmentVariants = cva(\n  \"relative min-w-0 border bg-card text-[13px]/[18px] text-card-foreground animate-in fade-in-0 zoom-in-98 duration-200 ease-out-quint [transition:border-color_120ms_ease-out,background-color_120ms_ease-out] data-[status=ready]:hover:border-border-strong motion-reduce:animate-none motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        row: \"flex items-center gap-2.5 rounded-xl p-2\",\n        card: \"grid w-44 grid-rows-[auto_auto] gap-2 rounded-[14px] p-1\",\n        chip: \"inline-flex min-h-[30px] max-w-[260px] items-center gap-1.5 rounded-full py-0.75 pr-0.75 pl-1\",\n      },\n    },\n  },\n);\n\nexport function Attachment({\n  variant = \"row\",\n  status = \"ready\",\n  progress,\n  mimeType = \"\",\n  children,\n  className,\n  ...props\n}: AttachmentProps) {\n  const id = useId();\n  const clamped = progress === undefined ? undefined : Math.min(100, Math.max(0, progress));\n  return (\n    <AttachmentContext.Provider value={{ id, variant, status, progress: clamped, mimeType }}>\n      {/* biome-ignore lint/a11y/useSemanticElements: an attachment groups its name, status, and actions; fieldset is for form controls. */}\n      <div\n        role=\"group\"\n        aria-labelledby={`${id}-name`}\n        data-slot=\"attachment\"\n        className={cn(\n          attachmentVariants({ variant }),\n          status === \"error\" && \"border-[color-mix(in_oklab,var(--destructive)_45%,var(--border))]\",\n          className,\n        )}\n        {...props}\n        aria-busy={status === \"uploading\" || undefined}\n        data-variant={variant}\n        data-status={status}\n      >\n        {children}\n      </div>\n    </AttachmentContext.Provider>\n  );\n}\n\nexport type AttachmentThumbnailProps = Omit<ComponentProps<\"span\">, \"children\"> & {\n  /** Image thumbnail URL. Without it, a file-type icon is shown. */\n  src?: string;\n  alt?: string;\n};\n\nexport function AttachmentThumbnail({\n  src,\n  alt = \"\",\n  className,\n  ...props\n}: AttachmentThumbnailProps) {\n  const context = useAttachment(\"AttachmentThumbnail\");\n  const Icon = context.status === \"error\" ? CircleAlert : iconFor(context.mimeType);\n  const chip = context.variant === \"chip\";\n  const card = context.variant === \"card\";\n  return (\n    <span\n      data-slot=\"attachment-thumbnail\"\n      className={cn(\n        \"relative grid flex-none place-items-center overflow-hidden\",\n        card && \"h-24 w-full rounded-[10px]\",\n        chip && \"size-5.5 rounded-full shadow-[0_0_0_1px_oklch(1_0_0/0.08)]\",\n        context.variant === \"row\" && \"size-9 rounded-lg\",\n        context.status === \"error\"\n          ? cn(\"bg-destructive/14\", dangerText)\n          : \"bg-muted text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {src && context.status !== \"error\" ? (\n        // biome-ignore lint/performance/noImgElement: registry components stay framework-neutral.\n        <img\n          src={src}\n          alt={alt}\n          className={cn(\n            \"size-full object-cover transition-opacity duration-200 ease-[ease-out] motion-reduce:transition-none\",\n            context.status === \"uploading\" ? \"opacity-60\" : \"opacity-100\",\n          )}\n        />\n      ) : (\n        <Icon size={card ? 20 : chip ? 12 : 16} strokeWidth={1.75} aria-hidden=\"true\" />\n      )}\n    </span>\n  );\n}\n\nexport function AttachmentDetails({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useAttachment(\"AttachmentDetails\");\n  return (\n    <div\n      data-slot=\"attachment-details\"\n      className={cn(\n        \"grid min-w-0 flex-auto\",\n        context.variant === \"chip\" ? \"gap-0 px-0.5\" : \"gap-px\",\n        context.variant === \"card\" && \"px-1.5 pb-1.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function AttachmentName({ className, ...props }: ComponentProps<\"span\">) {\n  const context = useAttachment(\"AttachmentName\");\n  return (\n    <span\n      data-slot=\"attachment-name\"\n      className={cn(\n        \"truncate font-medium\",\n        context.variant === \"chip\" ? \"text-[12.5px]/[18px]\" : \"text-[13px]/[18px]\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-name`}\n    />\n  );\n}\n\nexport function AttachmentMeta({ className, ...props }: ComponentProps<\"span\">) {\n  const context = useAttachment(\"AttachmentMeta\");\n  if (context.status === \"error\") return null;\n  return (\n    <span\n      data-slot=\"attachment-meta\"\n      className={cn(\n        \"truncate text-subtle-foreground tabular-nums\",\n        context.variant === \"chip\" ? \"text-[11px]/4\" : \"text-[11.5px]/4\",\n        context.status === \"uploading\" && \"shimmer-text motion-reduce:text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function AttachmentProgress({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useAttachment(\"AttachmentProgress\");\n  if (context.status !== \"uploading\") return null;\n  const value = context.progress;\n  return (\n    <Progress\n      aria-labelledby={`${context.id}-name`}\n      aria-valuenow={value}\n      aria-valuetext={value === undefined ? \"Uploading\" : `${Math.round(value)}% uploaded`}\n      data-slot=\"attachment-progress\"\n      data-indeterminate={value === undefined || undefined}\n      value={value ?? null}\n      className={cn(\n        \"mt-1.25 h-0.75 bg-muted\",\n        \"*:data-[slot=progress-indicator]:rounded-full *:data-[slot=progress-indicator]:bg-foreground *:data-[slot=progress-indicator]:transition-transform *:data-[slot=progress-indicator]:duration-240 *:data-[slot=progress-indicator]:ease-out-quint motion-reduce:*:data-[slot=progress-indicator]:transition-none\",\n        \"data-indeterminate:*:data-[slot=progress-indicator]:w-[35%] data-indeterminate:*:data-[slot=progress-indicator]:animate-[indeterminate_1.2s_cubic-bezier(0.65,0,0.35,1)_infinite] data-indeterminate:*:data-[slot=progress-indicator]:opacity-60\",\n        \"motion-reduce:data-indeterminate:*:data-[slot=progress-indicator]:animate-none motion-reduce:data-indeterminate:*:data-[slot=progress-indicator]:transform-none!\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function AttachmentError({ className, children, ...props }: ComponentProps<\"span\">) {\n  const context = useAttachment(\"AttachmentError\");\n  if (context.status !== \"error\") return null;\n  return (\n    <span\n      role=\"alert\"\n      data-slot=\"attachment-error\"\n      className={cn(\n        \"truncate\",\n        dangerText,\n        context.variant === \"chip\" ? \"text-[11px]/4\" : \"text-[11.5px]/4\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? \"Upload failed\"}\n    </span>\n  );\n}\n\nfunction controlClass(variant: AttachmentVariant, right: string) {\n  return cn(\n    \"flex-none cursor-pointer p-0 text-subtle-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-accent hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-ring active:scale-[0.92] motion-reduce:transition-none motion-reduce:active:scale-100 dark:hover:bg-accent\",\n    variant === \"chip\"\n      ? \"size-5.5 rounded-full [&_svg:not([class*='size-'])]:size-3\"\n      : \"size-7 rounded-lg [&_svg:not([class*='size-'])]:size-3.5\",\n    variant === \"card\" &&\n      cn(\n        \"absolute top-2.5 size-6 rounded-full bg-card/82 text-foreground shadow-[0_0_0_1px_color-mix(in_oklab,var(--foreground)_10%,transparent)] backdrop-blur-[8px]\",\n        right,\n      ),\n  );\n}\n\nexport function AttachmentRetry({ children, className, ...props }: ComponentProps<\"button\">) {\n  const context = useAttachment(\"AttachmentRetry\");\n  if (context.status !== \"error\") return null;\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label=\"Retry upload\"\n      title=\"Retry upload\"\n      aria-describedby={`${context.id}-name`}\n      data-slot=\"attachment-retry\"\n      className={cn(controlClass(context.variant, \"right-[38px]\"), className)}\n      {...props}\n    >\n      {children ?? <RotateCw size={context.variant === \"chip\" ? 12 : 14} aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n\nexport function AttachmentRemove({ children, className, ...props }: ComponentProps<\"button\">) {\n  const context = useAttachment(\"AttachmentRemove\");\n  const label = context.status === \"uploading\" ? \"Cancel upload\" : \"Remove attachment\";\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label={label}\n      title={label}\n      aria-describedby={`${context.id}-name`}\n      data-slot=\"attachment-remove\"\n      className={cn(controlClass(context.variant, \"right-2.5\"), className)}\n      {...props}\n    >\n      {children ?? <X size={context.variant === \"chip\" ? 12 : 14} aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/attachment.tsx"
    }
  ],
  "categories": [
    "ai",
    "files"
  ],
  "type": "registry:ui"
}