{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-uploader",
  "type": "registry:block",
  "title": "Image Uploader",
  "description": "A component for uploading multiple images with preview grid and progress indicators.",
  "dependencies": [
    "react-dropzone",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://edgestore.dev/r/dropzone.json",
    "https://edgestore.dev/r/progress-circle.json",
    "https://edgestore.dev/r/uploader-provider.json"
  ],
  "files": [
    {
      "path": "examples/components/src/components/upload/multi-image.tsx",
      "content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { Trash2Icon, XIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { type DropzoneOptions } from 'react-dropzone';\nimport { Dropzone } from './dropzone';\nimport { ProgressCircle } from './progress-circle';\nimport { useUploader } from './uploader-provider';\n\n/**\n * Props for the ImageList component.\n *\n * @interface ImageListProps\n * @extends {React.HTMLAttributes<HTMLDivElement>}\n */\nexport interface ImageListProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Whether the image deletion controls should be disabled.\n   */\n  disabled?: boolean;\n}\n\n/**\n * Displays a grid of image previews with upload status and controls.\n *\n * @component\n * @example\n * ```tsx\n * <ImageList className=\"my-4\" />\n * ```\n */\nconst ImageList = React.forwardRef<HTMLDivElement, ImageListProps>(\n  ({ className, disabled: initialDisabled, ...props }, ref) => {\n    const { fileStates, removeFile, cancelUpload } = useUploader();\n\n    // Create temporary URLs for image previews\n    const tempUrls = React.useMemo(() => {\n      const urls: Record<string, string> = {};\n      fileStates.forEach((fileState) => {\n        if (fileState.file) {\n          urls[fileState.key] = URL.createObjectURL(fileState.file);\n        }\n      });\n      return urls;\n    }, [fileStates]);\n\n    // Clean up temporary URLs on unmount\n    React.useEffect(() => {\n      return () => {\n        Object.values(tempUrls).forEach((url) => {\n          URL.revokeObjectURL(url);\n        });\n      };\n    }, [tempUrls]);\n\n    if (!fileStates.length) return null;\n\n    return (\n      <div\n        ref={ref}\n        className={cn('mt-4 grid grid-cols-3 gap-2', className)}\n        {...props}\n      >\n        {fileStates.map((fileState) => {\n          const displayUrl = tempUrls[fileState.key] ?? fileState.url;\n          return (\n            <div\n              key={fileState.key}\n              className={\n                'relative aspect-square h-full w-full rounded-md border-0 bg-muted p-0 shadow-md'\n              }\n            >\n              {displayUrl ? (\n                <img\n                  className=\"h-full w-full rounded-md object-cover\"\n                  src={displayUrl}\n                  alt={fileState.file.name}\n                />\n              ) : (\n                <div className=\"flex h-full w-full items-center justify-center bg-secondary\">\n                  <span className=\"text-xs text-muted-foreground\">\n                    No Preview\n                  </span>\n                </div>\n              )}\n\n              {/* Upload progress indicator */}\n              {fileState.status === 'UPLOADING' && (\n                <div className=\"absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-md bg-black/70\">\n                  <ProgressCircle progress={fileState.progress} />\n                </div>\n              )}\n\n              {/* Delete/cancel button */}\n              {displayUrl && !initialDisabled && (\n                <button\n                  type=\"button\"\n                  className=\"group pointer-events-auto absolute right-1 top-1 z-10 -translate-y-1/4 translate-x-1/4 transform rounded-full border border-muted-foreground bg-background p-1 shadow-md transition-all hover:scale-110\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    if (fileState.status === 'UPLOADING') {\n                      cancelUpload(fileState.key);\n                    } else {\n                      removeFile(fileState.key);\n                    }\n                  }}\n                >\n                  {fileState.status === 'UPLOADING' ? (\n                    <XIcon className=\"block h-4 w-4 text-muted-foreground\" />\n                  ) : (\n                    <Trash2Icon className=\"block h-4 w-4 text-muted-foreground\" />\n                  )}\n                </button>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    );\n  },\n);\nImageList.displayName = 'ImageList';\n\n/**\n * Props for the ImageDropzone component.\n *\n * @interface ImageDropzoneProps\n * @extends {React.HTMLAttributes<HTMLDivElement>}\n */\nexport interface ImageDropzoneProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Whether the dropzone is disabled.\n   */\n  disabled?: boolean;\n\n  /**\n   * Options passed to the underlying Dropzone component.\n   * Cannot include 'disabled' or 'onDrop' as they are handled internally.\n   */\n  dropzoneOptions?: Omit<DropzoneOptions, 'disabled' | 'onDrop'>;\n\n  /**\n   * Ref for the input element inside the Dropzone.\n   */\n  inputRef?: React.Ref<HTMLInputElement>;\n}\n\n/**\n * A dropzone component specifically for image uploads.\n *\n * @component\n * @example\n * ```tsx\n * <ImageDropzone\n *   dropzoneOptions={{\n *     maxFiles: 5,\n *     maxSize: 1024 * 1024 * 2, // 2MB\n *   }}\n * />\n * ```\n */\nconst ImageDropzone = React.forwardRef<HTMLDivElement, ImageDropzoneProps>(\n  ({ dropzoneOptions, className, disabled, inputRef, ...props }, ref) => {\n    return (\n      <div ref={ref} className={className} {...props}>\n        <Dropzone\n          ref={inputRef}\n          dropzoneOptions={{\n            accept: { 'image/*': [] },\n            ...dropzoneOptions,\n          }}\n          disabled={disabled}\n          dropMessageActive=\"Drop images here...\"\n          dropMessageDefault=\"drag & drop images here, or click to select\"\n        />\n      </div>\n    );\n  },\n);\nImageDropzone.displayName = 'ImageDropzone';\n\n/**\n * Props for the ImageUploader component.\n *\n * @interface ImageUploaderProps\n * @extends {React.HTMLAttributes<HTMLDivElement>}\n */\nexport interface ImageUploaderProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Maximum number of images allowed.\n   */\n  maxFiles?: number;\n\n  /**\n   * Maximum file size in bytes.\n   */\n  maxSize?: number;\n\n  /**\n   * Whether the uploader is disabled.\n   */\n  disabled?: boolean;\n\n  /**\n   * Additional className for the dropzone component.\n   */\n  dropzoneClassName?: string;\n\n  /**\n   * Additional className for the image list component.\n   */\n  imageListClassName?: string;\n\n  /**\n   * Ref for the input element inside the Dropzone.\n   */\n  inputRef?: React.Ref<HTMLInputElement>;\n}\n\n/**\n * A complete image uploader component with dropzone and image grid preview.\n *\n * @component\n * @example\n * ```tsx\n * <ImageUploader\n *   maxFiles={10}\n *   maxSize={1024 * 1024 * 5} // 5MB\n * />\n * ```\n */\nconst ImageUploader = React.forwardRef<HTMLDivElement, ImageUploaderProps>(\n  (\n    {\n      maxFiles,\n      maxSize,\n      disabled,\n      className,\n      dropzoneClassName,\n      imageListClassName,\n      inputRef,\n      ...props\n    },\n    ref,\n  ) => {\n    return (\n      <div ref={ref} className={cn('w-full space-y-4', className)} {...props}>\n        <ImageDropzone\n          ref={inputRef}\n          dropzoneOptions={{\n            maxFiles,\n            maxSize,\n          }}\n          disabled={disabled}\n          className={dropzoneClassName}\n        />\n\n        <ImageList className={imageListClassName} disabled={disabled} />\n      </div>\n    );\n  },\n);\nImageUploader.displayName = 'ImageUploader';\n\nexport { ImageList, ImageDropzone, ImageUploader };\n",
      "type": "registry:component",
      "target": "components/upload/multi-image.tsx"
    }
  ]
}