{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/file-upload/file-upload.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n/**\n * Angular port of the React FileUpload: a visually hidden `<input type=\"file\">` plus a\n * dashed dropzone (role=\"button\") that opens the picker on click / Enter / Space and\n * accepts dropped files. Single mode keeps the first file; `multiple` replaces the list\n * with the new selection (like React). `value` / `defaultValue` / `valueChange`\n * (`[(value)]`, a `File[]`).\n *\n * Slots (React props): `[slot=icon]` replaces the upload icon, the default content replaces\n * the \"Click to upload or drag and drop\" copy, and `[slot=content]` / `<ui-file-upload-content>`\n * renders below the dropzone (React `content`, e.g. the file list).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload, [ui-file-upload]',\n  standalone: true,\n  host: {\n    '[class]': 'hostClass',\n  },\n  template: `\n    <input\n      #input\n      type=\"file\"\n      [attr.accept]=\"accept ?? null\"\n      [multiple]=\"multiple\"\n      [disabled]=\"disabled\"\n      class=\"sr-only\"\n      tabindex=\"-1\"\n      (change)=\"handleFiles($any($event.target).files)\"\n    />\n    <div\n      role=\"button\"\n      [attr.tabindex]=\"disabled ? -1 : 0\"\n      [attr.aria-disabled]=\"disabled || null\"\n      [attr.aria-label]=\"multiple ? 'Upload files' : 'Upload file'\"\n      [class]=\"dropzoneClass\"\n      (click)=\"openFilePicker()\"\n      (keydown)=\"onDropzoneKeyDown($event)\"\n      (drop)=\"handleDrop($event)\"\n      (dragover)=\"handleDragOver($event)\"\n      (dragleave)=\"isDragging.set(false)\"\n    >\n      <ng-content select=\"[slot=icon]\"\n        ><svg\n          class=\"text-muted-foreground mb-2 size-10\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"1.5\"\n          viewBox=\"0 0 24 24\"\n        >\n          <path\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\"\n          /></svg\n      ></ng-content>\n      <ng-content\n        ><p class=\"text-muted-foreground text-sm\">\n          <span class=\"text-foreground font-semibold\">Click to upload</span> or drag and drop\n        </p>\n        @if (accept) {\n          <p class=\"text-muted-foreground/70 mt-1 text-xs\">{{ accept }}</p>\n        }\n      </ng-content>\n    </div>\n    <ng-content select=\"[slot=content], ui-file-upload-content, [ui-file-upload-content]\" />\n  `,\n})\nexport class UiFileUploadComponent {\n  private readonly _value = signal<File[] | undefined>(undefined)\n  private readonly _internal = signal<File[] | null>(null)\n  readonly isDragging = signal(false)\n\n  @Input() accept?: string\n  @Input({ transform: booleanAttribute }) multiple = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  /** Controlled file list. */\n  @Input()\n  set value(v: File[] | null | undefined) {\n    this._value.set(v ?? undefined)\n  }\n  get value(): File[] {\n    return this._value() ?? this._internal() ?? this.defaultValue ?? []\n  }\n  /** Uncontrolled initial file list. */\n  @Input() defaultValue?: File[]\n  @Output() valueChange = new EventEmitter<File[]>()\n  @Input('class') className?: string\n\n  @ViewChild('input', { static: true }) inputRef!: ElementRef<HTMLInputElement>\n\n  get hostClass(): string {\n    return cn('block space-y-3', this.className)\n  }\n\n  get dropzoneClass(): string {\n    return cn(\n      'border-muted-foreground/25 hover:border-muted-foreground/50 bg-muted/50 focus-visible:ring-ring flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n      this.isDragging() && 'border-primary bg-primary/5',\n      this.disabled && 'pointer-events-none opacity-50',\n    )\n  }\n\n  private emit(files: File[]): void {\n    if (this._value() === undefined) this._internal.set(files)\n    this.valueChange.emit(files)\n  }\n\n  handleFiles(files: FileList | null): void {\n    if (this.disabled || !files) return\n    const fileArray = Array.from(files)\n    const first = fileArray[0]\n    this.emit(this.multiple ? fileArray : first ? [first] : [])\n  }\n\n  handleDrop(e: DragEvent): void {\n    e.preventDefault()\n    this.isDragging.set(false)\n    if (this.disabled) return\n    this.handleFiles(e.dataTransfer?.files ?? null)\n  }\n\n  handleDragOver(e: DragEvent): void {\n    if (this.disabled) return\n    e.preventDefault()\n    this.isDragging.set(true)\n  }\n\n  /** Open the native file picker. */\n  openFilePicker(): void {\n    if (this.disabled) return\n    this.inputRef.nativeElement.click()\n  }\n\n  onDropzoneKeyDown(e: KeyboardEvent): void {\n    if (this.disabled) return\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      this.openFilePicker()\n    }\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload-trigger, [ui-file-upload-trigger]',\n  standalone: true,\n  host: { '[class]': 'hostClass' },\n  template: `<ng-content />`,\n})\nexport class UiFileUploadTriggerComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('block cursor-pointer', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload-content, [ui-file-upload-content]',\n  standalone: true,\n  host: { '[class]': 'hostClass' },\n  template: `<ng-content />`,\n})\nexport class UiFileUploadContentComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('block space-y-2', this.className)\n  }\n}\n\n/**\n * One file row (React FileUploadItem): file icon, name, size in KB and a remove button,\n * unless content is projected, which replaces the whole row body.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload-item, [ui-file-upload-item]',\n  standalone: true,\n  host: { '[class]': 'hostClass' },\n  template: `\n    <ng-content\n      ><svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        stroke-width=\"2\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n        class=\"lucide lucide-file text-muted-foreground size-8 shrink-0\"\n        aria-hidden=\"true\"\n      >\n        <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n        <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n      </svg>\n      <div class=\"min-w-0 flex-1\">\n        <p class=\"truncate text-sm font-medium\">{{ current?.name }}</p>\n        <p class=\"text-muted-foreground text-xs\">{{ ((current?.size ?? 0) / 1024).toFixed(1) }} KB</p>\n      </div>\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto rounded-sm transition-colors duration-200 focus-visible:ring-1 focus-visible:outline-none\"\n        (click)=\"remove.emit()\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          class=\"lucide lucide-x size-4\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M18 6 6 18\" />\n          <path d=\"m6 6 12 12\" />\n        </svg>\n        <span class=\"sr-only\">Remove file</span>\n      </button></ng-content\n    >\n  `,\n})\nexport class UiFileUploadItemComponent {\n  @Input({ required: true }) file!: File\n  /** `file` may still be unset on first render; the template reads it through this. */\n  get current(): File | undefined {\n    return this.file\n  }\n  /** React `onRemove`. */\n  @Output() remove = new EventEmitter<void>()\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('bg-muted/50 flex items-center gap-3 rounded-md border p-3', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload-item-name, [ui-file-upload-item-name]',\n  standalone: true,\n  host: { '[class]': 'hostClass' },\n  template: `<ng-content />`,\n})\nexport class UiFileUploadItemNameComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    // React renders a <p>; `block` gives the custom element the same box.\n    return cn('block truncate text-sm font-medium', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-file-upload-item-size, [ui-file-upload-item-size]',\n  standalone: true,\n  host: { '[class]': 'hostClass' },\n  template: `<ng-content />`,\n})\nexport class UiFileUploadItemSizeComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('text-muted-foreground text-xs', this.className)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/file-upload/file-upload.component.ts"
    },
    {
      "path": "packages/registry-angular/components/file-upload/index.ts",
      "content": "export {\n  UiFileUploadComponent,\n  UiFileUploadTriggerComponent,\n  UiFileUploadContentComponent,\n  UiFileUploadItemComponent,\n  UiFileUploadItemNameComponent,\n  UiFileUploadItemSizeComponent,\n} from './file-upload.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/file-upload/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Drag-and-drop file dropzone with click / Enter / Space to browse, file-type filtering and multi-file support, plus composable Content / Item (with remove) / ItemName / ItemSize parts for the file list. Wraps a native `<input type=\"file\">`.",
  "categories": [
    "form"
  ]
}