{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/file-upload/FileUpload.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n  accept?: string\n  multiple?: boolean\n  disabled?: boolean\n  modelValue?: File[]\n}>()\n\nconst emit = defineEmits<{\n  'update:modelValue': [files: File[]]\n}>()\n\nconst inputRef = ref<HTMLInputElement>()\nconst isDragging = ref(false)\n\nfunction handleFiles(files: FileList | null) {\n  if (!files) return\n  const fileArray = Array.from(files)\n  const first = fileArray[0]\n  emit('update:modelValue', props.multiple ? fileArray : first ? [first] : [])\n}\n\nfunction handleInputChange(e: Event) {\n  handleFiles((e.target as HTMLInputElement).files)\n}\n\nfunction handleDrop(e: DragEvent) {\n  isDragging.value = false\n  handleFiles(e.dataTransfer?.files ?? null)\n}\n\nfunction handleDragOver(e: DragEvent) {\n  e.preventDefault()\n  isDragging.value = true\n}\n\nfunction handleDragLeave() {\n  isDragging.value = false\n}\n\nfunction openFilePicker() {\n  inputRef.value?.click()\n}\n</script>\n\n<template>\n  <div :class=\"cn('space-y-3', props.class)\" v-bind=\"$attrs\">\n    <input\n      ref=\"inputRef\"\n      type=\"file\"\n      :accept=\"accept\"\n      :multiple=\"multiple\"\n      :disabled=\"disabled\"\n      class=\"sr-only\"\n      @change=\"handleInputChange\"\n    />\n\n    <div\n      :class=\"[\n        'border-muted-foreground/25 hover:border-muted-foreground/50 bg-muted/50 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors duration-200',\n        isDragging && 'border-primary bg-primary/5',\n        disabled && 'pointer-events-none opacity-50',\n      ]\"\n      @click=\"openFilePicker\"\n      @drop.prevent=\"handleDrop\"\n      @dragover=\"handleDragOver\"\n      @dragleave=\"handleDragLeave\"\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        <p v-if=\"accept\" class=\"text-muted-foreground/70 mt-1 text-xs\">{{ accept }}</p>\n      </slot>\n    </div>\n\n    <slot name=\"content\" />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUpload.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/FileUploadContent.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <div :class=\"cn('space-y-2', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUploadContent.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/FileUploadItem.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { FileIcon, X } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst file = defineModel<File>({ required: true })\n\ndefineEmits<{\n  (e: 'remove'): void\n}>()\n</script>\n\n<template>\n  <div :class=\"cn('bg-muted/50 flex items-center gap-3 rounded-md border p-3', props.class)\">\n    <FileIcon class=\"text-muted-foreground size-8 shrink-0\" />\n    <div class=\"min-w-0 flex-1\">\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    </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=\"$emit('remove')\"\n    >\n      <X class=\"size-4\" aria-hidden=\"true\" />\n      <span class=\"sr-only\">Remove file</span>\n    </button>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUploadItem.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/FileUploadItemName.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <p :class=\"cn('truncate text-sm font-medium', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </p>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUploadItemName.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/FileUploadItemSize.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <span :class=\"cn('text-muted-foreground text-xs', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </span>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUploadItemSize.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/FileUploadTrigger.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <div :class=\"cn('cursor-pointer', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/FileUploadTrigger.vue"
    },
    {
      "path": "packages/registry-vue/components/file-upload/index.ts",
      "content": "export { default as FileUpload } from './FileUpload.vue'\nexport { default as FileUploadTrigger } from './FileUploadTrigger.vue'\nexport { default as FileUploadContent } from './FileUploadContent.vue'\nexport { default as FileUploadItem } from './FileUploadItem.vue'\nexport { default as FileUploadItemName } from './FileUploadItemName.vue'\nexport { default as FileUploadItemSize } from './FileUploadItemSize.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/file-upload/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "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"
  ]
}