{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "type": "registry:component",
  "title": "File Upload",
  "description": "Dropzone-style file picker with previews for selected files.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/file-upload/file-upload.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { FileUp, UploadCloud, X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\n\nexport type FileUploadProps = Omit<\n  React.ComponentPropsWithoutRef<\"input\">,\n  \"onChange\" | \"type\" | \"value\"\n> & {\n  browseLabel?: string;\n  dropzoneText?: string;\n  files?: File[];\n  helperText?: string;\n  onFilesChange?: (files: File[]) => void;\n};\n\nfunction useFileUploadState(\n  controlledFiles: File[] | undefined,\n  multiple: boolean,\n  onFilesChange?: (files: File[]) => void,\n) {\n  const [internalFiles, setInternalFiles] = React.useState<File[]>(\n    controlledFiles ?? [],\n  );\n\n  React.useEffect(() => {\n    if (controlledFiles !== undefined) {\n      setInternalFiles(controlledFiles);\n    }\n  }, [controlledFiles]);\n\n  const resolvedFiles = controlledFiles ?? internalFiles;\n\n  const updateFiles = React.useCallback(\n    (nextFiles: File[]) => {\n      if (controlledFiles === undefined) {\n        setInternalFiles(nextFiles);\n      }\n\n      onFilesChange?.(nextFiles);\n    },\n    [controlledFiles, onFilesChange],\n  );\n\n  const addFiles = React.useCallback(\n    (incomingFiles: File[] | FileList) => {\n      const nextFiles = [...incomingFiles];\n      updateFiles(\n        multiple ? [...resolvedFiles, ...nextFiles] : nextFiles.slice(0, 1),\n      );\n    },\n    [multiple, resolvedFiles, updateFiles],\n  );\n\n  const removeFile = React.useCallback(\n    (fileToRemove: File) => {\n      updateFiles(\n        resolvedFiles.filter(\n          (file) =>\n            !(\n              file.name === fileToRemove.name &&\n              file.size === fileToRemove.size &&\n              file.lastModified === fileToRemove.lastModified\n            ),\n        ),\n      );\n    },\n    [resolvedFiles, updateFiles],\n  );\n\n  return { addFiles, removeFile, resolvedFiles };\n}\n\nfunction assignInputReference(\n  reference: React.ForwardedRef<HTMLInputElement>,\n  node: HTMLInputElement | null,\n) {\n  if (typeof reference === \"function\") {\n    reference(node);\n    return;\n  }\n\n  if (reference) {\n    reference.current = node;\n  }\n}\n\nfunction FileListItem({\n  file,\n  onRemove,\n}: {\n  file: File;\n  onRemove: () => void;\n}) {\n  return (\n    <li className=\"flex items-center justify-between rounded-md border bg-muted/30 px-3 py-2 text-sm\">\n      <div className=\"min-w-0\">\n        <p className=\"truncate font-medium\">{file.name}</p>\n        <p className=\"text-xs text-muted-foreground\">\n          {(file.size / 1024).toFixed(1)} KB\n        </p>\n      </div>\n      <Button\n        aria-label={`Remove ${file.name}`}\n        onClick={onRemove}\n        size=\"icon\"\n        type=\"button\"\n        variant=\"ghost\"\n      >\n        <X className=\"size-4\" />\n      </Button>\n    </li>\n  );\n}\n\ntype FileUploadDropzoneProps = {\n  browseLabel: string;\n  children: React.ReactNode;\n  disabled?: boolean;\n  dropzoneText: string;\n  helperText: string;\n  isDragging: boolean;\n  onActivate: () => void;\n  onDragStateChange: (dragging: boolean) => void;\n  onFilesDrop: (files: FileList) => void;\n};\n\nfunction FileUploadDropzone({\n  browseLabel,\n  children,\n  disabled,\n  dropzoneText,\n  helperText,\n  isDragging,\n  onActivate,\n  onDragStateChange,\n  onFilesDrop,\n}: FileUploadDropzoneProps) {\n  return (\n    <div\n      className={cn(\n        \"flex min-h-40 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-input bg-background px-6 py-8 text-center transition-colors\",\n        isDragging && \"border-primary bg-accent/40\",\n        disabled && \"cursor-not-allowed opacity-50\",\n      )}\n      onClick={onActivate}\n      onDragEnter={(event) => {\n        event.preventDefault();\n        if (!disabled) {\n          onDragStateChange(true);\n        }\n      }}\n      onDragLeave={(event) => {\n        event.preventDefault();\n        onDragStateChange(false);\n      }}\n      onDragOver={(event) => {\n        event.preventDefault();\n      }}\n      onDrop={(event) => {\n        event.preventDefault();\n        onDragStateChange(false);\n        if (!disabled && event.dataTransfer.files.length > 0) {\n          onFilesDrop(event.dataTransfer.files);\n        }\n      }}\n      onKeyDown={(event) => {\n        if ((event.key === \"Enter\" || event.key === \" \") && !disabled) {\n          event.preventDefault();\n          onActivate();\n        }\n      }}\n      role=\"button\"\n      tabIndex={disabled ? -1 : 0}\n    >\n      <UploadCloud className=\"mb-3 size-10 text-muted-foreground\" />\n      <div className=\"space-y-1\">\n        <p className=\"font-medium\">{dropzoneText}</p>\n        <p className=\"text-sm text-muted-foreground\">{helperText}</p>\n      </div>\n      <span className=\"mt-4 inline-flex h-10 items-center justify-center rounded-md border border-input bg-secondary px-4 py-2 text-sm font-medium text-secondary-foreground shadow-sm\">\n        <FileUp className=\"mr-2 size-4\" />\n        {browseLabel}\n      </span>\n      {children}\n    </div>\n  );\n}\n\nfunction FileUploadList({\n  files,\n  onRemove,\n}: {\n  files: File[];\n  onRemove: (file: File) => void;\n}) {\n  if (files.length === 0) {\n    return null;\n  }\n\n  return (\n    <ul className=\"space-y-2\">\n      {files.map((file) => (\n        <FileListItem\n          file={file}\n          key={`${file.name}-${file.lastModified}-${file.size}`}\n          onRemove={() => {\n            onRemove(file);\n          }}\n        />\n      ))}\n    </ul>\n  );\n}\n\nfunction FileUploadComponent(\n  {\n    accept,\n    browseLabel = \"Choose files\",\n    className,\n    disabled,\n    dropzoneText = \"Drag and drop files here, or click to browse.\",\n    files,\n    helperText = \"Supports one or more files.\",\n    multiple = true,\n    onFilesChange,\n    ...props\n  }: FileUploadProps,\n  reference: React.ForwardedRef<HTMLInputElement>,\n) {\n  const inputReference = React.useRef<HTMLInputElement | null>(null);\n  const [isDragging, setIsDragging] = React.useState(false);\n  const { addFiles, removeFile, resolvedFiles } = useFileUploadState(\n    files,\n    multiple,\n    onFilesChange,\n  );\n\n  return (\n    <div className={cn(\"space-y-3\", className)}>\n      <FileUploadDropzone\n        browseLabel={browseLabel}\n        disabled={disabled}\n        dropzoneText={dropzoneText}\n        helperText={helperText}\n        isDragging={isDragging}\n        onActivate={() => {\n          if (!disabled) {\n            inputReference.current?.click();\n          }\n        }}\n        onDragStateChange={setIsDragging}\n        onFilesDrop={addFiles}\n      >\n        <input\n          {...props}\n          accept={accept}\n          aria-label={browseLabel}\n          className=\"sr-only\"\n          disabled={disabled}\n          multiple={multiple}\n          onChange={(event) => {\n            if (event.target.files) {\n              addFiles(event.target.files);\n            }\n          }}\n          ref={(node) => {\n            inputReference.current = node;\n            assignInputReference(reference, node);\n          }}\n          type=\"file\"\n        />\n      </FileUploadDropzone>\n      <FileUploadList files={resolvedFiles} onRemove={removeFile} />\n    </div>\n  );\n}\n\nconst FileUpload = React.forwardRef(FileUploadComponent);\nFileUpload.displayName = \"FileUpload\";\n\nexport { FileUpload };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
