{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "type": "registry:ui",
  "description": "Drag-and-drop file dropzone with click-to-browse fallback, file-type filtering, multi-file support, and per-file progress + remove controls. Wraps native `<input type=\"file\">` with proper a11y.",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/file-upload/file-upload.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { File as FileIcon, X } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\n/* --------------------------------------------------------------- File Upload */\n\nexport class UipFileUpload extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    accept: { type: String },\n    multiple: { type: Boolean },\n    disabled: { type: Boolean },\n    files: { type: Array, attribute: false },\n    isDragging: { state: true },\n  }\n\n  accept?: string\n  multiple = false\n  disabled = false\n  files: File[] = []\n\n  isDragging = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload')\n  }\n\n  openFilePicker() {\n    if (this.disabled) return\n    const input = this.renderRoot.querySelector('input[type=file]') as HTMLInputElement | null\n    input?.click()\n  }\n\n  private handleFiles(fileList: FileList | null) {\n    if (this.disabled || !fileList) return\n    const newFiles = Array.from(fileList)\n    const updated = this.multiple ? [...this.files, ...newFiles] : newFiles.slice(0, 1)\n    this.files = updated\n    this.dispatchEvent(\n      new CustomEvent('value-change', {\n        detail: { files: updated },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n    this.requestUpdate()\n  }\n\n  private onInputChange(e: Event) {\n    const input = e.target as HTMLInputElement\n    this.handleFiles(input.files)\n    input.value = ''\n  }\n\n  private onDrop(e: DragEvent) {\n    e.preventDefault()\n    this.isDragging = false\n    if (this.disabled) return\n    this.handleFiles(e.dataTransfer?.files ?? null)\n  }\n\n  private onDragOver(e: DragEvent) {\n    if (this.disabled) return\n    e.preventDefault()\n    this.isDragging = true\n  }\n\n  private onDragLeave() {\n    this.isDragging = false\n  }\n\n  private onKeyDown(e: KeyboardEvent) {\n    if (this.disabled) return\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      this.openFilePicker()\n    }\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"space-y-3\">\n        <input\n          type=\"file\"\n          accept=${this.accept ?? nothing}\n          ?multiple=${this.multiple}\n          ?disabled=${this.disabled}\n          class=\"sr-only\"\n          tabindex=\"-1\"\n          @change=${this.onInputChange}\n        />\n\n        <div\n          role=\"button\"\n          tabindex=${this.disabled ? -1 : 0}\n          aria-disabled=${this.disabled ? 'true' : nothing}\n          aria-label=${this.multiple ? 'Upload files' : 'Upload file'}\n          class=${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 cursor-pointer',\n            this.isDragging && 'border-primary bg-primary/5',\n            this.disabled && 'pointer-events-none opacity-50',\n          )}\n          @click=${this.openFilePicker}\n          @keydown=${this.onKeyDown}\n          @drop=${this.onDrop}\n          @dragover=${this.onDragOver}\n          @dragleave=${this.onDragLeave}\n        >\n          <slot name=\"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              />\n            </svg>\n          </slot>\n          <slot>\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            ${this.accept\n              ? html`<p class=\"text-muted-foreground/70 mt-1 text-xs\">${this.accept}</p>`\n              : nothing}\n          </slot>\n        </div>\n\n        <slot name=\"content\"></slot>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------- File Upload Trigger */\n\nexport class UipFileUploadTrigger extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        cursor: pointer;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload-trigger')\n    this.addEventListener('click', () => {\n      const parent = this.closest('uip-file-upload') as UipFileUpload | null\n      parent?.openFilePicker()\n    })\n  }\n\n  render() {\n    return html`<slot></slot>`\n  }\n}\n\n/* ------------------------------------------------------- File Upload Content */\n\nexport class UipFileUploadContent extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload-content')\n  }\n\n  render() {\n    return html`<div class=\"space-y-2\"><slot></slot></div>`\n  }\n}\n\n/* ---------------------------------------------------------- File Upload Item */\n\nexport class UipFileUploadItem extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    file: { type: Object },\n    fileName: { type: String, attribute: 'file-name' },\n    fileSize: { type: Number, attribute: 'file-size' },\n  }\n\n  file?: File\n  fileName = ''\n  fileSize = 0\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload-item')\n  }\n\n  private onRemoveClick(e: Event) {\n    e.stopPropagation()\n    this.dispatchEvent(\n      new CustomEvent('remove', {\n        detail: { file: this.file },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  render() {\n    const name = this.file?.name ?? this.fileName\n    const size = this.file?.size ?? this.fileSize\n    const formattedSize = (size / 1024).toFixed(1) + ' KB'\n\n    return html`\n      <div\n        part=\"base\"\n        class=\"bg-muted/50 border-border flex items-center gap-3 rounded-md border p-3 text-sm\"\n      >\n        <slot>\n          ${icon(FileIcon, 'file-icon', 'text-muted-foreground size-8 shrink-0')}\n          <div class=\"min-w-0 flex-1\">\n            <p class=\"truncate font-medium text-foreground\">${name}</p>\n            <p class=\"text-muted-foreground text-xs\">${formattedSize}</p>\n          </div>\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto rounded-sm p-1 transition-colors focus-visible:ring-1 focus-visible:outline-none\"\n            aria-label=\"Remove file\"\n            @click=${this.onRemoveClick}\n          >\n            ${icon(X, 'x', 'size-4')}\n          </button>\n        </slot>\n      </div>\n    `\n  }\n}\n\n/* ----------------------------------------------------- File Upload Item Name */\n\nexport class UipFileUploadItemName extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload-item-name')\n  }\n\n  render() {\n    return html`<p class=\"truncate text-sm font-medium text-foreground\"><slot></slot></p>`\n  }\n}\n\n/* ----------------------------------------------------- File Upload Item Size */\n\nexport class UipFileUploadItemSize extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'file-upload-item-size')\n  }\n\n  render() {\n    return html`<span class=\"text-muted-foreground text-xs\"><slot></slot></span>`\n  }\n}\n\n/* -------------------------------------------------------------- Registration */\n\ncustomElements.get('uip-file-upload') || customElements.define('uip-file-upload', UipFileUpload)\ncustomElements.get('uip-file-upload-trigger') || customElements.define('uip-file-upload-trigger', UipFileUploadTrigger)\ncustomElements.get('uip-file-upload-content') || customElements.define('uip-file-upload-content', UipFileUploadContent)\ncustomElements.get('uip-file-upload-item') || customElements.define('uip-file-upload-item', UipFileUploadItem)\ncustomElements.get('uip-file-upload-item-name') || customElements.define('uip-file-upload-item-name', UipFileUploadItemName)\ncustomElements.get('uip-file-upload-item-size') || customElements.define('uip-file-upload-item-size', UipFileUploadItemSize)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-file-upload': UipFileUpload\n    'uip-file-upload-trigger': UipFileUploadTrigger\n    'uip-file-upload-content': UipFileUploadContent\n    'uip-file-upload-item': UipFileUploadItem\n    'uip-file-upload-item-name': UipFileUploadItemName\n    'uip-file-upload-item-size': UipFileUploadItemSize\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/file-upload/file-upload.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
