{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "type": "registry:ui",
  "title": "Attachment",
  "description": "A file someone attached, as a small raised plate: type, name, size or upload progress; it lands on the object spring when added, fills while uploading, says when it failed, and leaves on release when removed.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/attachment/attachment.tsx",
      "type": "registry:ui",
      "target": "components/metalui/attachment/attachment.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Progress as BaseProgress } from '@base-ui/react/progress';\nimport { IconButton } from '../icon-button/icon-button';\nimport { Button } from '../button/button';\n\n/* ─────────────────────────────────────────────────────────\n * ATTACHMENT, a file someone attached, as a small raised plate\n *\n *   rest      its type engraved in a sunk well; its name with the middle cut (the extension stays\n *             readable); a line with its size\n *   arrive    it lands (T5b): from one nest above onto the table on the object spring, with its\n *             small overshoot\n *   upload    a thin track under the name fills with the progress fill on the settle spring; the\n *             line says how far\n *   failed    the line says so in red, with Try again\n *   remove    it leaves as rows do (T9): one nest down, fading, on the release spring, then goes\n * Reduce Motion: it appears and goes at once; the fill still moves.\n * An object: it stands for a person's file. It uses the raised surface, the well and the progress fill.\n * ───────────────────────────────────────────────────────── */\n\nconst PLATE = 'mu-attachment relative flex w-full items-center gap-attachment-gap h-attachment-height min-w-attachment-min-width max-w-attachment-max-width p-attachment-pad rounded-attachment-radius recipe-surface-raise-sm attachment-land data-leaving:attachment-leave reduced-motion:animate-none';\nconst TYPE = 'mu-attachment-type grid flex-none place-items-center size-attachment-type-size rounded-attachment-type-radius recipe-well-field type-label text-ink2 uppercase';\nconst BODY = 'mu-attachment-body grid flex-1 min-w-0 gap-1';\nconst NAME = 'mu-attachment-name flex min-w-0 type-ui text-ink';\nconst META = 'mu-attachment-meta type-meta tabular-nums text-ink3 data-failed:text-form-field-error-ink';\nconst TRACK = 'mu-attachment-track block h-attachment-track-height rounded-pill overflow-hidden recipe-switch';\nconst FILL = 'block h-full rounded-pill recipe-switch-on transition-progress-fill';\n\n/** \"12.4 MB\" from bytes. */\nexport function formatBytes(bytes: number, locale?: string) {\n  const units = ['B', 'KB', 'MB', 'GB'];\n  let n = bytes, u = 0;\n  while (n >= 1000 && u < units.length - 1) { n /= 1000; u++; }\n  return `${new Intl.NumberFormat(locale, { maximumFractionDigits: n < 10 && u > 0 ? 1 : 0 }).format(n)} ${units[u]}`;\n}\n\nexport interface AttachmentProps {\n  name: string;\n  /** Size in bytes. */\n  size?: number;\n  /** 0–100 while uploading; leave it out once done. */\n  progress?: number;\n  /** The upload failed: say why in a few words (\"Too large\"). */\n  error?: string;\n  onRetry?: () => void;\n  /** Shows the remove key; called after the file has left. */\n  onRemove?: () => void;\n  className?: string;\n}\n\n/** A file someone attached. (Named Attachment so it never shadows the browser's File.) */\nexport function Attachment({ name, size, progress, error, onRetry, onRemove, className }: AttachmentProps) {\n  const [leaving, setLeaving] = React.useState(false);\n  const plate = React.useRef<HTMLDivElement>(null);\n  const dot = name.lastIndexOf('.');\n  const base = dot > 0 ? name.slice(0, dot) : name;\n  const ext = dot > 0 ? name.slice(dot) : '';\n  const type = ext.slice(1, 5) || 'file';\n  const uploading = progress != null && !error;\n  const meta = error ? error : uploading ? `Uploading · ${Math.round(progress)} %` : size != null ? formatBytes(size) : '';\n\n  const remove = () => {\n    const el = plate.current;\n    const ms = el ? parseFloat(getComputedStyle(el).getPropertyValue('--mu-spring-release-d')) * 1000 : 0;\n    const reduced = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (!ms || reduced) return onRemove?.();\n    setLeaving(true);\n    window.setTimeout(() => onRemove?.(), ms);\n  };\n\n  return (\n    <div ref={plate} role=\"group\" aria-label={name} data-leaving={leaving ? '' : undefined} className={className ? `${PLATE} ${className}` : PLATE}>\n      <span aria-hidden className={TYPE}>{type}</span>\n      <span className={BODY}>\n        <span className={NAME} title={name}><span className=\"truncate\">{base}</span><span className=\"flex-none\">{ext}</span></span>\n        {uploading && (\n          <BaseProgress.Root value={progress} aria-label={`Uploading ${name}`}>\n            <BaseProgress.Track className={TRACK}><BaseProgress.Indicator className={FILL} /></BaseProgress.Track>\n          </BaseProgress.Root>\n        )}\n        <span className={META} data-failed={error ? '' : undefined} role={error ? 'alert' : undefined}>{meta}</span>\n      </span>\n      {error && onRetry && <Button size=\"compact\" onClick={onRetry}>Try again</Button>}\n      {onRemove && (\n        <IconButton\n          variant=\"mini\"\n          label={`Remove ${name}`}\n          onClick={remove}\n          icon={<svg aria-hidden viewBox=\"0 0 10 10\" className=\"size-attachment-remove-glyph\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\"><path d=\"M2.5 2.5l5 5M7.5 2.5l-5 5\" /></svg>}\n        />\n      )}\n    </div>\n  );\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/attachment.md. SwiftUI: MetalAttachment in the MetalUI Swift package."
}
