{ "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\";\nimport { Button } from \"@/registry/bases/base/ui/button\";\nimport {\n Cropper,\n CropperArea,\n type CropperAreaData,\n CropperImage,\n type CropperPoint,\n type CropperProps,\n} from \"@/registry/bases/base/ui/cropper\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/registry/bases/base/ui/dialog\";\nimport {\n FileUpload,\n FileUploadDropzone,\n FileUploadItem,\n FileUploadItemDelete,\n FileUploadItemMetadata,\n FileUploadItemPreview,\n FileUploadList,\n FileUploadTrigger,\n} from \"@/registry/bases/base/ui/file-upload\";\nimport { Label } from \"@/registry/bases/base/ui/label\";\nimport { Slider } from \"@/registry/bases/base/ui/slider\";\n\nasync function createCroppedImage(\n imageSrc: string,\n cropData: CropperAreaData,\n fileName: string,\n): Promise {\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([]);\n const [filesWithCrops, setFilesWithCrops] = React.useState<\n Map\n >(new Map());\n const [selectedFile, setSelectedFile] = React.useState(null);\n const [crop, setCrop] = React.useState({ x: 0, y: 0 });\n const [zoom, setZoom] = React.useState(1);\n const [croppedArea, setCroppedArea] = React.useState(\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 =\n React.useCallback((_, croppedAreaPixels) => {\n setCroppedArea(croppedAreaPixels);\n }, []);\n\n const onCropComplete: NonNullable =\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 \n \n
\n \n
\n

\n Drop images here or click to upload\n

\n

\n PNG, JPG, WebP up to 10MB\n

\n
\n (\n \n )}\n />\n
\n
\n \n {files.map((file) => {\n const fileWithCrop = filesWithCrops.get(file.name);\n\n return (\n \n React.ReactNode,\n ) => {\n if (\n fileWithCrop?.cropped &&\n originalFile.type.startsWith(\"image/\")\n ) {\n const url = URL.createObjectURL(fileWithCrop.cropped);\n return (\n // biome-ignore lint/performance/noImgElement: dynamic cropped file URLs from user uploads don't work well with Next.js Image optimization\n \n );\n }\n\n return fallback();\n }}\n />\n \n
\n \n onFileSelect(file)}\n >\n \n \n }\n />\n \n \n Crop Image\n \n Adjust the crop area and zoom level for{\" \"}\n {selectedFile?.name}\n \n \n {selectedFile && selectedImageUrl && (\n
\n \n \n \n \n
\n \n \n setZoom(\n Array.isArray(value) ? (value[0] ?? 1) : value,\n )\n }\n min={1}\n max={3}\n step={0.1}\n className=\"w-full\"\n />\n
\n
\n )}\n \n \n \n \n
\n \n (\n \n \n \n )}\n />\n
\n
\n );\n })}\n
\n \n );\n}\n", "target": "" } ] }