{
  "name": "cropper-file-upload-demo",
  "type": "registry:example",
  "dependencies": [
    "@hookform/resolvers",
    "react-hook-form",
    "zod"
  ],
  "registryDependencies": [
    "cropper",
    "button",
    "dialog",
    "label",
    "slider"
  ],
  "files": [
    {
      "path": "examples/cropper-file-upload-demo.tsx",
      "type": "registry:example",
      "content": "\"use client\";\n\nimport { CropIcon, UploadIcon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport { Button } from \"@/registry/bases/radix/ui/button\";\nimport {\n  Cropper,\n  CropperArea,\n  type CropperAreaData,\n  CropperImage,\n  type CropperPoint,\n  type CropperProps,\n} from \"@/registry/bases/radix/ui/cropper\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/registry/bases/radix/ui/dialog\";\nimport {\n  FileUpload,\n  FileUploadDropzone,\n  FileUploadItem,\n  FileUploadItemDelete,\n  FileUploadItemMetadata,\n  FileUploadItemPreview,\n  FileUploadList,\n  FileUploadTrigger,\n} from \"@/registry/bases/radix/ui/file-upload\";\nimport { Label } from \"@/registry/bases/radix/ui/label\";\nimport { Slider } from \"@/registry/bases/radix/ui/slider\";\n\nasync function createCroppedImage(\n  imageSrc: string,\n  cropData: CropperAreaData,\n  fileName: string,\n): Promise<File> {\n  const image = new Image();\n  image.crossOrigin = \"anonymous\";\n\n  return new Promise((resolve, reject) => {\n    image.onload = () => {\n      const canvas = document.createElement(\"canvas\");\n      const ctx = canvas.getContext(\"2d\");\n\n      if (!ctx) {\n        reject(new Error(\"Could not get canvas context\"));\n        return;\n      }\n\n      canvas.width = cropData.width;\n      canvas.height = cropData.height;\n\n      ctx.drawImage(\n        image,\n        cropData.x,\n        cropData.y,\n        cropData.width,\n        cropData.height,\n        0,\n        0,\n        cropData.width,\n        cropData.height,\n      );\n\n      canvas.toBlob((blob) => {\n        if (!blob) {\n          reject(new Error(\"Canvas is empty\"));\n          return;\n        }\n\n        const croppedFile = new File([blob], `cropped-${fileName}`, {\n          type: \"image/png\",\n        });\n        resolve(croppedFile);\n      }, \"image/png\");\n    };\n\n    image.onerror = () => reject(new Error(\"Failed to load image\"));\n    image.src = imageSrc;\n  });\n}\n\ninterface FileWithCrop {\n  original: File;\n  cropped?: File;\n}\n\nexport default function CropperFileUploadDemo() {\n  const [files, setFiles] = React.useState<File[]>([]);\n  const [filesWithCrops, setFilesWithCrops] = React.useState<\n    Map<string, FileWithCrop>\n  >(new Map());\n  const [selectedFile, setSelectedFile] = React.useState<File | null>(null);\n  const [crop, setCrop] = React.useState<CropperPoint>({ x: 0, y: 0 });\n  const [zoom, setZoom] = React.useState(1);\n  const [croppedArea, setCroppedArea] = React.useState<CropperAreaData | null>(\n    null,\n  );\n  const [showCropDialog, setShowCropDialog] = React.useState(false);\n\n  const selectedImageUrl = React.useMemo(() => {\n    if (!selectedFile) return null;\n    return URL.createObjectURL(selectedFile);\n  }, [selectedFile]);\n\n  React.useEffect(() => {\n    return () => {\n      if (selectedImageUrl) {\n        URL.revokeObjectURL(selectedImageUrl);\n      }\n    };\n  }, [selectedImageUrl]);\n\n  const onFilesChange = React.useCallback((newFiles: File[]) => {\n    setFiles(newFiles);\n\n    setFilesWithCrops((prevFilesWithCrops) => {\n      const updatedFilesWithCrops = new Map(prevFilesWithCrops);\n\n      for (const file of newFiles) {\n        if (!updatedFilesWithCrops.has(file.name)) {\n          updatedFilesWithCrops.set(file.name, { original: file });\n        }\n      }\n\n      const fileNames = new Set(newFiles.map((f) => f.name));\n      for (const [fileName] of updatedFilesWithCrops) {\n        if (!fileNames.has(fileName)) {\n          updatedFilesWithCrops.delete(fileName);\n        }\n      }\n\n      return updatedFilesWithCrops;\n    });\n  }, []);\n\n  const onFileSelect = React.useCallback(\n    (file: File) => {\n      const fileWithCrop = filesWithCrops.get(file.name);\n      const originalFile = fileWithCrop?.original ?? file;\n\n      setSelectedFile(originalFile);\n      setCrop({ x: 0, y: 0 });\n      setZoom(1);\n      setCroppedArea(null);\n      setShowCropDialog(true);\n    },\n    [filesWithCrops],\n  );\n\n  const onCropAreaChange: NonNullable<CropperProps[\"onCropAreaChange\"]> =\n    React.useCallback((_, croppedAreaPixels) => {\n      setCroppedArea(croppedAreaPixels);\n    }, []);\n\n  const onCropComplete: NonNullable<CropperProps[\"onCropComplete\"]> =\n    React.useCallback((_, croppedAreaPixels) => {\n      setCroppedArea(croppedAreaPixels);\n    }, []);\n\n  const onCropReset = React.useCallback(() => {\n    setCrop({ x: 0, y: 0 });\n    setZoom(1);\n    setCroppedArea(null);\n  }, []);\n\n  const onCropDialogOpenChange = React.useCallback((open: boolean) => {\n    if (!open) {\n      setShowCropDialog(false);\n      setCrop({ x: 0, y: 0 });\n      setZoom(1);\n      setCroppedArea(null);\n    }\n  }, []);\n\n  const onCropApply = React.useCallback(async () => {\n    if (!selectedFile || !croppedArea || !selectedImageUrl) return;\n\n    try {\n      const croppedFile = await createCroppedImage(\n        selectedImageUrl,\n        croppedArea,\n        selectedFile.name,\n      );\n\n      const newFilesWithCrops = new Map(filesWithCrops);\n      const existing = newFilesWithCrops.get(selectedFile.name);\n      if (existing) {\n        newFilesWithCrops.set(selectedFile.name, {\n          ...existing,\n          cropped: croppedFile,\n        });\n        setFilesWithCrops(newFilesWithCrops);\n      }\n\n      onCropDialogOpenChange(false);\n    } catch (error) {\n      toast.error(\n        error instanceof Error ? error.message : \"Failed to crop image\",\n      );\n    }\n  }, [\n    selectedFile,\n    croppedArea,\n    selectedImageUrl,\n    filesWithCrops,\n    onCropDialogOpenChange,\n  ]);\n\n  return (\n    <FileUpload\n      value={files}\n      onValueChange={onFilesChange}\n      accept=\"image/*\"\n      maxFiles={2}\n      maxSize={10 * 1024 * 1024}\n      multiple\n      className=\"w-full max-w-lg\"\n    >\n      <FileUploadDropzone className=\"min-h-32\">\n        <div className=\"flex flex-col items-center gap-2 text-center\">\n          <UploadIcon className=\"size-8 text-muted-foreground\" />\n          <div>\n            <p className=\"text-sm font-medium\">\n              Drop images here or click to upload\n            </p>\n            <p className=\"text-xs text-muted-foreground\">\n              PNG, JPG, WebP up to 10MB\n            </p>\n          </div>\n          <FileUploadTrigger asChild>\n            <Button variant=\"outline\" size=\"sm\">\n              Choose Files\n            </Button>\n          </FileUploadTrigger>\n        </div>\n      </FileUploadDropzone>\n      <FileUploadList className=\"max-h-96 overflow-y-auto\">\n        {files.map((file) => {\n          const fileWithCrop = filesWithCrops.get(file.name);\n\n          return (\n            <FileUploadItem key={file.name} value={file}>\n              <FileUploadItemPreview\n                render={(originalFile, fallback) => {\n                  if (\n                    fileWithCrop?.cropped &&\n                    originalFile.type.startsWith(\"image/\")\n                  ) {\n                    const url = URL.createObjectURL(fileWithCrop.cropped);\n                    return (\n                      <img\n                        src={url}\n                        alt={originalFile.name}\n                        className=\"size-full object-cover\"\n                      />\n                    );\n                  }\n\n                  return fallback();\n                }}\n              />\n              <FileUploadItemMetadata />\n              <div className=\"flex gap-1\">\n                <Dialog\n                  open={showCropDialog}\n                  onOpenChange={onCropDialogOpenChange}\n                >\n                  <DialogTrigger asChild>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      className=\"size-8\"\n                      onClick={() => onFileSelect(file)}\n                    >\n                      <CropIcon />\n                    </Button>\n                  </DialogTrigger>\n                  <DialogContent className=\"max-w-4xl\">\n                    <DialogHeader>\n                      <DialogTitle>Crop Image</DialogTitle>\n                      <DialogDescription>\n                        Adjust the crop area and zoom level for{\" \"}\n                        {selectedFile?.name}\n                      </DialogDescription>\n                    </DialogHeader>\n                    {selectedFile && selectedImageUrl && (\n                      <div className=\"flex flex-col gap-4\">\n                        <Cropper\n                          aspectRatio={1}\n                          shape=\"circle\"\n                          crop={crop}\n                          onCropChange={setCrop}\n                          zoom={zoom}\n                          onZoomChange={setZoom}\n                          onCropAreaChange={onCropAreaChange}\n                          onCropComplete={onCropComplete}\n                          className=\"h-96\"\n                        >\n                          <CropperImage\n                            src={selectedImageUrl}\n                            alt={selectedFile.name}\n                            crossOrigin=\"anonymous\"\n                          />\n                          <CropperArea />\n                        </Cropper>\n                        <div className=\"flex flex-col gap-2\">\n                          <Label className=\"text-sm\">\n                            Zoom: {zoom.toFixed(2)}\n                          </Label>\n                          <Slider\n                            value={[zoom]}\n                            onValueChange={(value) => setZoom(value[0] ?? 1)}\n                            min={1}\n                            max={3}\n                            step={0.1}\n                            className=\"w-full\"\n                          />\n                        </div>\n                      </div>\n                    )}\n                    <DialogFooter>\n                      <Button onClick={onCropReset} variant=\"outline\">\n                        Reset\n                      </Button>\n                      <Button onClick={onCropApply} disabled={!croppedArea}>\n                        Crop\n                      </Button>\n                    </DialogFooter>\n                  </DialogContent>\n                </Dialog>\n                <FileUploadItemDelete asChild>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    className=\"hover:text-destructive-foreground dark:hover:text-destructive-foreground size-8 hover:bg-destructive/30 dark:hover:bg-destructive\"\n                  >\n                    <XIcon />\n                  </Button>\n                </FileUploadItemDelete>\n              </div>\n            </FileUploadItem>\n          );\n        })}\n      </FileUploadList>\n    </FileUpload>\n  );\n}\n",
      "target": ""
    }
  ]
}