{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drop-zone",
  "title": "Drop zone",
  "author": "Tori Bryan",
  "description": "Wraps an area people can drop files onto, with an overlay while they drag and files split by type.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/drop-zone.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { UploadIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype DropZoneProps = React.ComponentProps<\"div\"> & {\n  /** Called with the dropped files that pass `accept`. */\n  onDropFiles?: (files: File[]) => void\n  /** Called with the dropped files that fail `accept`, to say why they were left out. */\n  onRejectFiles?: (files: File[]) => void\n  /** File types it takes, as in a file input's accept attribute. */\n  accept?: string\n  /** Takes no files and shows nothing while true. */\n  disabled?: boolean\n  /** What happens on drop, shown over the area while files are dragged across it. */\n  label?: React.ReactNode\n  /** The limits, such as the types and size it takes. */\n  description?: React.ReactNode\n}\n\n/*\n * Wraps the area people can drop files onto and shows an overlay while they\n * drag. It has no other state, and it never touches what it covers: the\n * files go to whoever passed onDropFiles.\n */\nfunction DropZone({\n  onDropFiles,\n  onRejectFiles,\n  accept,\n  disabled,\n  label = \"Drop files to attach\",\n  description,\n  className,\n  children,\n  onDragEnter,\n  onDragOver,\n  onDragLeave,\n  onDrop,\n  ...props\n}: DropZoneProps) {\n  const [dragging, setDragging] = React.useState(false)\n  // dragenter and dragleave fire for every child crossed, so count them.\n  const depth = React.useRef(0)\n\n  const hasFiles = (event: React.DragEvent) =>\n    !disabled && event.dataTransfer.types.includes(\"Files\")\n\n  return (\n    <div\n      data-slot=\"drop-zone\"\n      data-dragging={dragging ? \"\" : undefined}\n      className={cn(\"relative\", className)}\n      onDragEnter={(event) => {\n        // A caller's handler runs first and can take the drag over by\n        // calling preventDefault.\n        onDragEnter?.(event)\n        if (event.defaultPrevented || !hasFiles(event)) return\n        event.preventDefault()\n        depth.current++\n        setDragging(true)\n      }}\n      onDragOver={(event) => {\n        onDragOver?.(event)\n        if (event.defaultPrevented || !hasFiles(event)) return\n        event.preventDefault()\n        event.dataTransfer.dropEffect = \"copy\"\n      }}\n      onDragLeave={(event) => {\n        onDragLeave?.(event)\n        depth.current = Math.max(0, depth.current - 1)\n        if (depth.current === 0) setDragging(false)\n      }}\n      onDrop={(event) => {\n        onDrop?.(event)\n        // Reset first, so the overlay never sticks if the zone was disabled\n        // mid-drag.\n        depth.current = 0\n        setDragging(false)\n        if (event.defaultPrevented || !hasFiles(event)) return\n        event.preventDefault()\n        const files = Array.from(event.dataTransfer.files)\n        const accepted = files.filter((file) => matchesAccept(file, accept))\n        const rejected = files.filter((file) => !accepted.includes(file))\n        if (accepted.length) onDropFiles?.(accepted)\n        if (rejected.length) onRejectFiles?.(rejected)\n      }}\n      {...props}\n    >\n      {children}\n      {dragging ? (\n        <div\n          aria-hidden\n          data-slot=\"drop-zone-overlay\"\n          className=\"pointer-events-none absolute inset-0 z-10 rounded-[inherit] bg-popover-overlay p-2\"\n        >\n          <div className=\"flex size-full flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-ring p-4 text-center\">\n            <UploadIcon className=\"size-6 text-muted-foreground\" />\n            <div className=\"flex flex-col gap-1 text-sm\">\n              <span className=\"font-medium text-foreground\">{label}</span>\n              {description ? (\n                <span className=\"text-muted-foreground\">{description}</span>\n              ) : null}\n            </div>\n          </div>\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\n/** Whether a file passes an accept string such as \"image/*,.pdf\". */\nfunction matchesAccept(file: File, accept?: string) {\n  if (!accept?.trim()) return true\n  const name = file.name.toLowerCase()\n  const type = file.type.toLowerCase()\n  return accept.split(\",\").some((raw) => {\n    const rule = raw.trim().toLowerCase()\n    if (!rule) return false\n    if (rule.startsWith(\".\")) return name.endsWith(rule)\n    if (rule.endsWith(\"/*\")) return type.startsWith(rule.slice(0, -1))\n    return type === rule\n  })\n}\n\nexport { DropZone, matchesAccept, type DropZoneProps }\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "color-popover-overlay": "var(--popover-overlay)"
    },
    "light": {
      "popover-overlay": "color-mix(in oklch, var(--color-white) 85%, transparent)"
    },
    "dark": {
      "popover-overlay": "color-mix(in oklch, var(--color-neutral-900) 85%, transparent)"
    }
  },
  "meta": {
    "tier": "base-components",
    "group": "Forms",
    "docs": "https://fibo.toribryan.com/?path=/docs/base-components-forms-drop-zone--docs",
    "status": "new"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/base-components-forms-drop-zone--docs",
  "categories": [
    "base-components",
    "forms"
  ],
  "type": "registry:ui"
}