import { useCallback, useEffect, useRef } from "react"; import ReactCrop, { type Crop } from "react-image-crop"; import "react-image-crop/dist/ReactCrop.css"; export interface CropCanvasProps { imageSrc: string; crop: Crop; aspect: number | undefined; showGrid: boolean; imgDimensions: { width: number; height: number } | null; onCropChange: (crop: Crop) => void; onImageLoad: (dims: { width: number; height: number }) => void; } export function CropCanvas({ imageSrc, crop, aspect, showGrid, imgDimensions, onCropChange, onImageLoad, }: CropCanvasProps) { const containerRef = useRef(null); const imgRef = useRef(null); const handleImageLoad = useCallback( (e: React.SyntheticEvent) => { const img = e.currentTarget; onImageLoad({ width: img.naturalWidth, height: img.naturalHeight }); }, [onImageLoad], ); // Keyboard nudging useEffect(() => { const el = containerRef.current; if (!el) return; const handleKeyDown = (e: KeyboardEvent) => { const step = e.shiftKey ? 10 : 1; const { naturalWidth, naturalHeight } = imgRef.current ?? { naturalWidth: 0, naturalHeight: 0, }; if (!naturalWidth || !naturalHeight) return; // Convert step from pixels to percentage const stepX = (step / naturalWidth) * 100; const stepY = (step / naturalHeight) * 100; let dx = 0; let dy = 0; if (e.key === "ArrowLeft") dx = -stepX; else if (e.key === "ArrowRight") dx = stepX; else if (e.key === "ArrowUp") dy = -stepY; else if (e.key === "ArrowDown") dy = stepY; else if (e.key === "Escape") { // Reset to full image onCropChange({ unit: "%", x: 0, y: 0, width: 100, height: 100, }); e.preventDefault(); return; } else if (e.key === "Enter") { // Submit the crop form (find and submit closest form) const form = document.querySelector("form[data-crop-form]"); if (form) form.requestSubmit(); e.preventDefault(); return; } else return; e.preventDefault(); onCropChange({ ...crop, x: Math.max(0, Math.min(100 - crop.width, crop.x + dx)), y: Math.max(0, Math.min(100 - crop.height, crop.y + dy)), }); }; el.addEventListener("keydown", handleKeyDown); return () => el.removeEventListener("keydown", handleKeyDown); }, [crop, onCropChange]); // Auto-focus the container on mount useEffect(() => { containerRef.current?.focus(); }, []); // Calculate pixel dimensions for the badge const pixelWidth = imgDimensions ? Math.round((crop.width / 100) * imgDimensions.width) : 0; const pixelHeight = imgDimensions ? Math.round((crop.height / 100) * imgDimensions.height) : 0; return (
{/* Crop area */}
onCropChange(percentCrop)} aspect={aspect} className="max-h-full" ruleOfThirds={showGrid} > Crop preview
{/* Info bar */}
Crop region: {pixelWidth} x {pixelHeight} {imgDimensions && ( Original: {imgDimensions.width} x {imgDimensions.height} )}
); }