{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "single-image-dropzone",
  "type": "registry:component",
  "title": "Single Image Dropzone",
  "description": "A dropzone component for uploading a single image with preview and progress indicator.",
  "dependencies": [
    "react-dropzone",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://edgestore.dev/r/progress-circle.json",
    "https://edgestore.dev/r/uploader-provider.json"
  ],
  "files": [
    {
      "path": "examples/components/src/components/upload/single-image.tsx",
      "content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport {\n  AlertCircleIcon,\n  Trash2Icon,\n  UploadCloudIcon,\n  XIcon,\n} from 'lucide-react';\nimport * as React from 'react';\nimport { useDropzone, type DropzoneOptions } from 'react-dropzone';\nimport { ProgressCircle } from './progress-circle';\nimport { formatFileSize, useUploader } from './uploader-provider';\n\nconst DROPZONE_VARIANTS = {\n  base: 'relative rounded-md p-4 flex justify-center items-center flex-col cursor-pointer min-h-[150px] min-w-[200px] border-2 border-dashed border-muted-foreground transition-colors duration-200 ease-in-out',\n  image: 'border-0 p-0 min-h-0 min-w-0 relative bg-muted shadow-md',\n  active: 'border-primary',\n  disabled:\n    'bg-muted/50 border-muted-foreground/50 cursor-default pointer-events-none',\n  accept: 'border-primary bg-primary/10',\n  reject: 'border-destructive bg-destructive/10',\n};\n\n/**\n * Props for the SingleImageDropzone component.\n *\n * @interface SingleImageDropzoneProps\n * @extends {React.HTMLAttributes<HTMLInputElement>}\n */\nexport interface SingleImageDropzoneProps\n  extends React.HTMLAttributes<HTMLInputElement> {\n  /**\n   * The width of the dropzone area in pixels.\n   */\n  width: number;\n\n  /**\n   * The height of the dropzone area in pixels.\n   */\n  height: number;\n\n  /**\n   * Whether the dropzone is disabled.\n   */\n  disabled?: boolean;\n\n  /**\n   * Options passed to the underlying react-dropzone component.\n   * Cannot include 'disabled', 'onDrop', 'maxFiles', or 'multiple' as they are handled internally.\n   */\n  dropzoneOptions?: Omit<\n    DropzoneOptions,\n    'disabled' | 'onDrop' | 'maxFiles' | 'multiple'\n  >;\n}\n\n/**\n * A single image upload component with preview and upload status.\n *\n * This component allows users to upload a single image, shows a preview,\n * displays upload progress, and provides controls to remove or cancel the upload.\n *\n * @component\n * @example\n * ```tsx\n * <SingleImageDropzone\n *   width={320}\n *   height={320}\n *   dropzoneOptions={{ maxSize: 1024 * 1024 * 2 }} // 2MB\n * />\n * ```\n */\nconst SingleImageDropzone = React.forwardRef<\n  HTMLInputElement,\n  SingleImageDropzoneProps\n>(({ dropzoneOptions, width, height, className, disabled, ...props }, ref) => {\n  const { fileStates, addFiles, removeFile, cancelUpload } = useUploader();\n  const [error, setError] = React.useState<string>();\n\n  const fileState = React.useMemo(() => fileStates[0], [fileStates]);\n  const maxSize = dropzoneOptions?.maxSize;\n\n  // Create temporary URL for image preview before upload is complete\n  const tempUrl = React.useMemo(() => {\n    if (fileState?.file) {\n      return URL.createObjectURL(fileState.file);\n    }\n    return null;\n  }, [fileState]);\n\n  // Clean up temporary URL to prevent memory leaks\n  React.useEffect(() => {\n    return () => {\n      if (tempUrl) {\n        URL.revokeObjectURL(tempUrl);\n      }\n    };\n  }, [tempUrl]);\n\n  const displayUrl = tempUrl ?? fileState?.url;\n  const isDisabled =\n    !!disabled ||\n    fileState?.status === 'UPLOADING' ||\n    fileState?.status === 'COMPLETE'; // Disable when upload complete\n\n  const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } =\n    useDropzone({\n      accept: { 'image/*': [] }, // Accept only image files\n      multiple: false,\n      disabled: isDisabled,\n      onDrop: (acceptedFiles, rejectedFiles) => {\n        setError(undefined);\n\n        // Handle rejections first\n        if (rejectedFiles.length > 0) {\n          if (rejectedFiles[0]?.errors[0]) {\n            const error = rejectedFiles[0].errors[0];\n            const code = error.code;\n\n            // User-friendly error messages\n            const messages: Record<string, string> = {\n              'file-too-large': `The file is too large. Max size is ${formatFileSize(\n                maxSize ?? 0,\n              )}.`,\n              'file-invalid-type': 'Invalid file type.',\n              'too-many-files': 'You can only upload one file.',\n              default: 'The file is not supported.',\n            };\n\n            setError(messages[code] ?? messages.default);\n          }\n          return; // Exit early if there are any rejections\n        }\n\n        // Handle accepted files only if there are no rejections\n        if (acceptedFiles.length > 0) {\n          // Remove existing file before adding a new one\n          if (fileStates[0]) {\n            removeFile(fileStates[0].key);\n          }\n          addFiles(acceptedFiles);\n        }\n      },\n      ...dropzoneOptions,\n    });\n\n  const dropZoneClassName = React.useMemo(\n    () =>\n      cn(\n        DROPZONE_VARIANTS.base,\n        isFocused && DROPZONE_VARIANTS.active,\n        isDisabled && DROPZONE_VARIANTS.disabled,\n        displayUrl && DROPZONE_VARIANTS.image,\n        isDragReject && DROPZONE_VARIANTS.reject,\n        isDragAccept && DROPZONE_VARIANTS.accept,\n        className,\n      ),\n    [isFocused, isDisabled, displayUrl, isDragAccept, isDragReject, className],\n  );\n\n  // Combined error message from dropzone or file state\n  const errorMessage = error ?? fileState?.error;\n\n  return (\n    <div className=\"flex flex-col items-center\">\n      <div\n        {...getRootProps({\n          className: dropZoneClassName,\n          style: {\n            width,\n            height,\n          },\n        })}\n      >\n        <input ref={ref} {...getInputProps()} {...props} />\n\n        {displayUrl ? (\n          <img\n            className=\"h-full w-full rounded-md object-cover\"\n            src={displayUrl}\n            alt={fileState?.file.name ?? 'uploaded image'}\n          />\n        ) : (\n          // Placeholder content shown when no image is selected\n          <div\n            className={cn(\n              'flex flex-col items-center justify-center gap-2 text-center text-xs text-muted-foreground',\n              isDisabled && 'opacity-50',\n            )}\n          >\n            <UploadCloudIcon className=\"mb-1 h-7 w-7\" />\n            <div className=\"font-medium\">\n              drag & drop an image or click to select\n            </div>\n            {maxSize && (\n              <div className=\"text-xs\">Max size: {formatFileSize(maxSize)}</div>\n            )}\n          </div>\n        )}\n\n        {/* Upload progress overlay */}\n        {displayUrl && fileState?.status === 'UPLOADING' && (\n          <div className=\"absolute inset-0 flex flex-col items-center justify-center rounded-md bg-black/70\">\n            <ProgressCircle progress={fileState.progress} />\n          </div>\n        )}\n\n        {/* Remove/Cancel button */}\n        {displayUrl &&\n          !disabled &&\n          fileState &&\n          fileState.status !== 'COMPLETE' && (\n            <button\n              type=\"button\"\n              className=\"group pointer-events-auto absolute right-1 top-1 z-10 transform rounded-full border border-muted-foreground bg-background p-1 shadow-md transition-all hover:scale-110\"\n              onClick={(e) => {\n                e.stopPropagation(); // Prevent triggering dropzone click\n                if (fileState.status === 'UPLOADING') {\n                  cancelUpload(fileState.key);\n                } else {\n                  removeFile(fileState.key);\n                  setError(undefined); // Clear any error when removing the file\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      {/* Error message display */}\n      {errorMessage && (\n        <div className=\"mt-2 flex items-center text-xs text-destructive\">\n          <AlertCircleIcon className=\"mr-1 h-4 w-4\" />\n          <span>{errorMessage}</span>\n        </div>\n      )}\n    </div>\n  );\n});\nSingleImageDropzone.displayName = 'SingleImageDropzone';\n\nexport { SingleImageDropzone };\n",
      "type": "registry:component",
      "target": "components/upload/single-image.tsx"
    }
  ]
}