{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/file-upload/FileUpload.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadProps extends HTMLAttributes<HTMLDivElement> {\n    accept?: string\n    multiple?: boolean\n    disabled?: boolean\n    /** Primary file list API (React `value` parity). Bindable — wins over `files` when both are set. */\n    value?: File[]\n    /** Uncontrolled initial file list (React parity). */\n    defaultValue?: File[]\n    /** Selected files. Bindable — the Svelte counterpart of Vue's `v-model`.\n     * @deprecated Use `value`. Kept as an alias — both stay functional and in sync. */\n    files?: File[]\n    /** Overrides the default upload icon in the dropzone. */\n    icon?: Snippet\n    /** Rendered below the dropzone — typically a `FileUploadContent` file list. */\n    content?: Snippet\n    ref?: HTMLDivElement | null\n    onValueChange?: (files: File[]) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    accept,\n    multiple = false,\n    disabled = false,\n    value = $bindable<File[]>(),\n    defaultValue,\n    files = $bindable<File[]>(),\n    icon,\n    content,\n    children,\n    ref = $bindable(null),\n    onValueChange,\n    ...restProps\n  }: FileUploadProps = $props()\n\n  let inputRef = $state<HTMLInputElement | null>(null)\n  let isDragging = $state(false)\n\n  // Seed initial value so bound consumers never see undefined on mount.\n  // svelte-ignore state_referenced_locally\n  const initial = defaultValue ?? value ?? files ?? []\n  if (value === undefined) value = initial\n  if (files === undefined) files = initial\n\n  let inner = $state<File[]>(initial)\n\n  // Dual-bindable sync (`value` primary, `files` deprecated alias): last writer\n  // wins, with `value` winning when both change in the same tick.\n  // svelte-ignore state_referenced_locally\n  let prevValue = value\n  // svelte-ignore state_referenced_locally\n  let prevFiles = files\n\n  $effect(() => {\n    const v = value\n    const f = files\n    const vChanged = v !== prevValue\n    const fChanged = f !== prevFiles\n    if (vChanged && v !== undefined) {\n      prevValue = v\n      inner = v\n      if (f !== v) {\n        files = v\n        prevFiles = v\n      }\n    } else if (fChanged && f !== undefined) {\n      prevFiles = f\n      inner = f\n      if (v !== f) {\n        value = f\n        prevValue = f\n      }\n    } else {\n      if (vChanged) prevValue = v\n      if (fChanged) prevFiles = f\n    }\n  })\n\n  function commit(next: File[]) {\n    inner = next\n    prevValue = next\n    prevFiles = next\n    value = next\n    files = next\n    onValueChange?.(next)\n  }\n\n  function handleFiles(list: FileList | null) {\n    if (disabled || !list) return\n    const fileArray = Array.from(list)\n    const first = fileArray[0]\n    commit(multiple ? fileArray : first ? [first] : [])\n  }\n\n  function handleInputChange(e: Event) {\n    handleFiles((e.target as HTMLInputElement).files)\n  }\n\n  function handleDrop(e: DragEvent) {\n    isDragging = false\n    if (disabled) return\n    handleFiles(e.dataTransfer?.files ?? null)\n  }\n\n  function handleDragOver(e: DragEvent) {\n    if (disabled) return\n    e.preventDefault()\n    isDragging = true\n  }\n\n  function handleDragLeave() {\n    isDragging = false\n  }\n\n  function openFilePicker() {\n    if (disabled) return\n    inputRef?.click()\n  }\n\n  function onDropzoneKeydown(e: KeyboardEvent) {\n    if (disabled) return\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      openFilePicker()\n    }\n  }\n</script>\n\n<div bind:this={ref} class={cn('space-y-3', className)} data-uipkge data-slot=\"file-upload\" {...restProps}>\n  <input\n    bind:this={inputRef}\n    type=\"file\"\n    {accept}\n    {multiple}\n    {disabled}\n    class=\"sr-only\"\n    tabindex=\"-1\"\n    onchange={handleInputChange}\n  />\n\n  <div\n    role=\"button\"\n    tabindex={disabled ? -1 : 0}\n    aria-disabled={disabled || undefined}\n    aria-label={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',\n      isDragging && 'border-primary bg-primary/5',\n      disabled && 'pointer-events-none opacity-50',\n    )}\n    onclick={openFilePicker}\n    onkeydown={onDropzoneKeydown}\n    ondrop={(e) => {\n      e.preventDefault()\n      handleDrop(e)\n    }}\n    ondragover={handleDragOver}\n    ondragleave={handleDragLeave}\n  >\n    {#if icon}\n      {@render icon()}\n    {:else}\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    {/if}\n    {#if children}\n      {@render children()}\n    {:else}\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      {/if}\n    {/if}\n  </div>\n\n  {@render content?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUpload.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/FileUploadContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadContentProps extends HTMLAttributes<HTMLDivElement> {\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: FileUploadContentProps = $props()\n</script>\n\n<div bind:this={ref} class={cn('space-y-2', className)} data-uipkge data-slot=\"file-upload-content\" {...restProps}>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUploadContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/FileUploadItem.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadItemProps extends HTMLAttributes<HTMLDivElement> {\n    file: File\n    onremove?: () => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { File as FileIcon, X } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n\n  let { class: className, file, onremove, children, ref = $bindable(null), ...restProps }: FileUploadItemProps = $props()\n</script>\n\n<div\n  bind:this={ref}\n  class={cn('bg-muted/50 flex items-center gap-3 rounded-md border p-3', className)}\n  data-uipkge\n  data-slot=\"file-upload-item\"\n  {...restProps}\n>\n  <FileIcon class=\"text-muted-foreground size-8 shrink-0\" />\n  <div class=\"min-w-0 flex-1\">\n    {#if children}\n      {@render children()}\n    {:else}\n      <p class=\"truncate text-sm font-medium\">{file.name}</p>\n      <p class=\"text-muted-foreground text-xs\">{(file.size / 1024).toFixed(1)} KB</p>\n    {/if}\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    onclick={() => onremove?.()}\n  >\n    <X class=\"size-4\" aria-hidden=\"true\" />\n    <span class=\"sr-only\">Remove file</span>\n  </button>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUploadItem.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/FileUploadItemName.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadItemNameProps extends HTMLAttributes<HTMLParagraphElement> {\n    ref?: HTMLParagraphElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: FileUploadItemNameProps = $props()\n</script>\n\n<p bind:this={ref} class={cn('truncate text-sm font-medium', className)} data-uipkge data-slot=\"file-upload-item-name\" {...restProps}>\n  {@render children?.()}\n</p>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUploadItemName.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/FileUploadItemSize.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadItemSizeProps extends HTMLAttributes<HTMLSpanElement> {\n    ref?: HTMLSpanElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: FileUploadItemSizeProps = $props()\n</script>\n\n<span bind:this={ref} class={cn('text-muted-foreground text-xs', className)} data-uipkge data-slot=\"file-upload-item-size\" {...restProps}>\n  {@render children?.()}\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUploadItemSize.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/FileUploadTrigger.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface FileUploadTriggerProps extends HTMLAttributes<HTMLDivElement> {\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: FileUploadTriggerProps = $props()\n</script>\n\n<div bind:this={ref} class={cn('cursor-pointer', className)} data-uipkge data-slot=\"file-upload-trigger\" {...restProps}>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/FileUploadTrigger.svelte"
    },
    {
      "path": "packages/registry-svelte/components/file-upload/index.ts",
      "content": "export { default as FileUpload, type FileUploadProps } from './FileUpload.svelte'\nexport { default as FileUploadTrigger, type FileUploadTriggerProps } from './FileUploadTrigger.svelte'\nexport { default as FileUploadContent, type FileUploadContentProps } from './FileUploadContent.svelte'\nexport { default as FileUploadItem, type FileUploadItemProps } from './FileUploadItem.svelte'\nexport { default as FileUploadItemName, type FileUploadItemNameProps } from './FileUploadItemName.svelte'\nexport { default as FileUploadItemSize, type FileUploadItemSizeProps } from './FileUploadItemSize.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/file-upload/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}