{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "description": "Composable dropzone with file list, keyboard support and size/count limits.",
  "dependencies": [
    "react-dropzone",
    "sonner",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "components/ui/file-upload.tsx",
      "content": "'use client'\n\nimport { Input } from '@/components/ui/input'\nimport { cn } from '@/lib/utils'\nimport {\n  Dispatch,\n  SetStateAction,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport {\n  useDropzone,\n  DropzoneState,\n  FileRejection,\n  DropzoneOptions,\n} from 'react-dropzone'\nimport { toast } from 'sonner'\nimport { Trash2 as RemoveIcon } from 'lucide-react'\nimport { buttonVariants } from '@/components/ui/button'\n\ntype DirectionOptions = 'rtl' | 'ltr' | undefined\n\ntype FileUploaderContextType = {\n  dropzoneState: DropzoneState\n  isLOF: boolean\n  isFileTooBig: boolean\n  removeFileFromSet: (index: number) => void\n  activeIndex: number\n  setActiveIndex: Dispatch<SetStateAction<number>>\n  orientation: 'horizontal' | 'vertical'\n  direction: DirectionOptions\n}\n\nconst FileUploaderContext = createContext<FileUploaderContextType | null>(null)\n\nexport const useFileUpload = () => {\n  const context = useContext(FileUploaderContext)\n  if (!context) {\n    throw new Error('useFileUpload must be used within a FileUploaderProvider')\n  }\n  return context\n}\n\ntype FileUploaderProps = {\n  value: File[] | null\n  reSelect?: boolean\n  onValueChange: (value: File[] | null) => void\n  dropzoneOptions: DropzoneOptions\n  orientation?: 'horizontal' | 'vertical'\n}\n\nexport const FileUploader = forwardRef<\n  HTMLDivElement,\n  FileUploaderProps & React.HTMLAttributes<HTMLDivElement>\n>(\n  (\n    {\n      className,\n      dropzoneOptions,\n      value,\n      onValueChange,\n      reSelect,\n      orientation = 'vertical',\n      children,\n      dir,\n      ...props\n    },\n    ref,\n  ) => {\n    const [isFileTooBig, setIsFileTooBig] = useState(false)\n    const [isLOF, setIsLOF] = useState(false)\n    const [activeIndex, setActiveIndex] = useState(-1)\n    const {\n      accept = {\n        'image/*': ['.jpg', '.jpeg', '.png', '.gif'],\n      },\n      maxFiles = 1,\n      maxSize = 4 * 1024 * 1024,\n      multiple = true,\n    } = dropzoneOptions\n\n    const reSelectAll = maxFiles === 1 ? true : reSelect\n    const direction: DirectionOptions = dir === 'rtl' ? 'rtl' : 'ltr'\n\n    const removeFileFromSet = useCallback(\n      (i: number) => {\n        if (!value) return\n        const newFiles = value.filter((_, index) => index !== i)\n        onValueChange(newFiles)\n      },\n      [value, onValueChange],\n    )\n\n    const handleKeyDown = useCallback(\n      (e: React.KeyboardEvent<HTMLDivElement>) => {\n        e.preventDefault()\n        e.stopPropagation()\n\n        if (!value) return\n\n        const moveNext = () => {\n          const nextIndex = activeIndex + 1\n          setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex)\n        }\n\n        const movePrev = () => {\n          const nextIndex = activeIndex - 1\n          setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex)\n        }\n\n        const prevKey =\n          orientation === 'horizontal'\n            ? direction === 'ltr'\n              ? 'ArrowLeft'\n              : 'ArrowRight'\n            : 'ArrowUp'\n\n        const nextKey =\n          orientation === 'horizontal'\n            ? direction === 'ltr'\n              ? 'ArrowRight'\n              : 'ArrowLeft'\n            : 'ArrowDown'\n\n        if (e.key === nextKey) {\n          moveNext()\n        } else if (e.key === prevKey) {\n          movePrev()\n        } else if (e.key === 'Enter' || e.key === 'Space') {\n          if (activeIndex === -1) {\n            dropzoneState.inputRef.current?.click()\n          }\n        } else if (e.key === 'Delete' || e.key === 'Backspace') {\n          if (activeIndex !== -1) {\n            removeFileFromSet(activeIndex)\n            if (value.length - 1 === 0) {\n              setActiveIndex(-1)\n              return\n            }\n            movePrev()\n          }\n        } else if (e.key === 'Escape') {\n          setActiveIndex(-1)\n        }\n      },\n      [value, activeIndex, removeFileFromSet],\n    )\n\n    const onDrop = useCallback(\n      (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {\n        const files = acceptedFiles\n\n        if (!files) {\n          toast.error('file error , probably too big')\n          return\n        }\n\n        const newValues: File[] = value ? [...value] : []\n\n        if (reSelectAll) {\n          newValues.splice(0, newValues.length)\n        }\n\n        files.forEach((file) => {\n          if (newValues.length < maxFiles) {\n            newValues.push(file)\n          }\n        })\n\n        onValueChange(newValues)\n\n        if (rejectedFiles.length > 0) {\n          for (let i = 0; i < rejectedFiles.length; i++) {\n            if (rejectedFiles[i].errors[0]?.code === 'file-too-large') {\n              toast.error(\n                `File is too large. Max size is ${maxSize / 1024 / 1024}MB`,\n              )\n              break\n            }\n            if (rejectedFiles[i].errors[0]?.message) {\n              toast.error(rejectedFiles[i].errors[0].message)\n              break\n            }\n          }\n        }\n      },\n      [reSelectAll, value],\n    )\n\n    useEffect(() => {\n      if (!value) return\n      if (value.length === maxFiles) {\n        setIsLOF(true)\n        return\n      }\n      setIsLOF(false)\n    }, [value, maxFiles])\n\n    const opts = dropzoneOptions\n      ? dropzoneOptions\n      : { accept, maxFiles, maxSize, multiple }\n\n    const dropzoneState = useDropzone({\n      ...opts,\n      onDrop,\n      onDropRejected: () => setIsFileTooBig(true),\n      onDropAccepted: () => setIsFileTooBig(false),\n    })\n\n    return (\n      <FileUploaderContext.Provider\n        value={{\n          dropzoneState,\n          isLOF,\n          isFileTooBig,\n          removeFileFromSet,\n          activeIndex,\n          setActiveIndex,\n          orientation,\n          direction,\n        }}\n      >\n        <div\n          ref={ref}\n          tabIndex={0}\n          onKeyDownCapture={handleKeyDown}\n          className={cn(\n            'grid w-full focus:outline-none overflow-hidden ',\n            className,\n            {\n              'gap-2': value && value.length > 0,\n            },\n          )}\n          dir={dir}\n          {...props}\n        >\n          {children}\n        </div>\n      </FileUploaderContext.Provider>\n    )\n  },\n)\n\nFileUploader.displayName = 'FileUploader'\n\nexport const FileUploaderContent = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ children, className, ...props }, ref) => {\n  const { orientation } = useFileUpload()\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  return (\n    <div\n      className={cn('w-full px-1')}\n      ref={containerRef}\n      aria-description=\"content file holder\"\n    >\n      <div\n        {...props}\n        ref={ref}\n        className={cn(\n          'flex rounded-xl gap-1',\n          orientation === 'horizontal' ? 'flex-raw flex-wrap' : 'flex-col',\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  )\n})\n\nFileUploaderContent.displayName = 'FileUploaderContent'\n\nexport const FileUploaderItem = forwardRef<\n  HTMLDivElement,\n  { index: number } & React.HTMLAttributes<HTMLDivElement>\n>(({ className, index, children, ...props }, ref) => {\n  const { removeFileFromSet, activeIndex, direction } = useFileUpload()\n  const isSelected = index === activeIndex\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        buttonVariants({ variant: 'ghost' }),\n        'h-6 p-1 justify-between cursor-pointer relative',\n        className,\n        isSelected ? 'bg-muted' : '',\n      )}\n      {...props}\n    >\n      <div className=\"font-medium leading-none tracking-tight flex items-center gap-1.5 h-full w-full\">\n        {children}\n      </div>\n      <button\n        type=\"button\"\n        className={cn(\n          'absolute',\n          direction === 'rtl' ? 'top-1 left-1' : 'top-1 right-1',\n        )}\n        onClick={() => removeFileFromSet(index)}\n      >\n        <span className=\"sr-only\">remove item {index}</span>\n        <RemoveIcon className=\"w-4 h-4 hover:stroke-destructive duration-200 ease-in-out\" />\n      </button>\n    </div>\n  )\n})\n\nFileUploaderItem.displayName = 'FileUploaderItem'\n\nexport const FileInput = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, children, ...props }, ref) => {\n  const { dropzoneState, isFileTooBig, isLOF } = useFileUpload()\n  const rootProps = isLOF ? {} : dropzoneState.getRootProps()\n  return (\n    <div\n      ref={ref}\n      {...props}\n      className={`relative w-full ${\n        isLOF ? 'opacity-50 cursor-not-allowed ' : 'cursor-pointer '\n      }`}\n    >\n      <div\n        className={cn(\n          `w-full rounded-lg duration-300 ease-in-out\n         ${\n           dropzoneState.isDragAccept\n             ? 'border-green-500'\n             : dropzoneState.isDragReject || isFileTooBig\n               ? 'border-red-500'\n               : 'border-gray-300'\n         }`,\n          className,\n        )}\n        {...rootProps}\n      >\n        {children}\n      </div>\n      <Input\n        ref={dropzoneState.inputRef}\n        disabled={isLOF}\n        {...dropzoneState.getInputProps()}\n        className={`${isLOF ? 'cursor-not-allowed' : ''}`}\n      />\n    </div>\n  )\n})\n\nFileInput.displayName = 'FileInput'\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input",
    "upload"
  ],
  "type": "registry:ui"
}