/** * @ai-summary Post-capture crop tool with draggable handles * @ai-context Allows user to adjust crop area with touch/mouse, confirm or retake */ import React, { useCallback, useState } from 'react'; import { Box, IconButton, Button, Typography, CircularProgress } from '@mui/material'; import CheckIcon from '@mui/icons-material/Check'; import RefreshIcon from '@mui/icons-material/Refresh'; import ReplayIcon from '@mui/icons-material/Replay'; import SkipNextIcon from '@mui/icons-material/SkipNext'; import type { CropToolProps } from './types'; import { useImageCrop, type CropHandle } from './useImageCrop'; export const CropTool: React.FC = ({ imageSrc, lockAspectRatio = false, aspectRatio, onConfirm, onReset, onRetake, onSkip, }) => { const [isProcessing, setIsProcessing] = useState(false); const { cropArea, isDragging, resetCrop, executeCrop, handleDragStart } = useImageCrop({ aspectRatio: lockAspectRatio ? aspectRatio : undefined, }); const handleConfirm = useCallback(async () => { setIsProcessing(true); try { const croppedBlob = await executeCrop(imageSrc); onConfirm(croppedBlob); } catch (error) { console.error('Failed to crop image:', error); // On error, skip cropping and use original onSkip(); } finally { setIsProcessing(false); } }, [executeCrop, imageSrc, onConfirm, onSkip]); const handleReset = useCallback(() => { resetCrop(); onReset(); }, [resetCrop, onReset]); return ( {/* Image with crop overlay */} {/* Source image */} Captured {/* Dark overlay outside crop area */} {/* Top overlay */} {/* Bottom overlay */} {/* Left overlay */} {/* Right overlay */} {/* Crop area with handles */} {/* Move handle (center area) */} {/* Corner handles */} {/* Edge handles */} {/* Grid lines for alignment */} {Array.from({ length: 9 }).map((_, i) => ( ))} {/* Instructions */} Drag to adjust crop area {/* Action buttons */} {isProcessing ? ( ) : ( )} ); }; interface CropHandleProps { handle: CropHandle; onDragStart: (handle: CropHandle, event: React.MouseEvent | React.TouchEvent) => void; position: | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top' | 'bottom' | 'left' | 'right'; } const CropHandle: React.FC = ({ handle, onDragStart, position }) => { const handleSize = 24; const handleVisualSize = 12; const positionStyles: Record = { 'top-left': { top: -handleSize / 2, left: -handleSize / 2, cursor: 'nwse-resize', }, 'top-right': { top: -handleSize / 2, right: -handleSize / 2, cursor: 'nesw-resize', }, 'bottom-left': { bottom: -handleSize / 2, left: -handleSize / 2, cursor: 'nesw-resize', }, 'bottom-right': { bottom: -handleSize / 2, right: -handleSize / 2, cursor: 'nwse-resize', }, top: { top: -handleSize / 2, left: '50%', transform: 'translateX(-50%)', cursor: 'ns-resize', }, bottom: { bottom: -handleSize / 2, left: '50%', transform: 'translateX(-50%)', cursor: 'ns-resize', }, left: { top: '50%', left: -handleSize / 2, transform: 'translateY(-50%)', cursor: 'ew-resize', }, right: { top: '50%', right: -handleSize / 2, transform: 'translateY(-50%)', cursor: 'ew-resize', }, }; const isCorner = position.includes('-'); return ( onDragStart(handle, e)} onTouchStart={(e) => onDragStart(handle, e)} sx={{ position: 'absolute', width: handleSize, height: handleSize, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, ...positionStyles[position], }} > ); }; interface CropHandleAreaProps { handle: CropHandle; onDragStart: (handle: CropHandle, event: React.MouseEvent | React.TouchEvent) => void; sx?: React.CSSProperties & { [key: string]: unknown }; } const CropHandleArea: React.FC = ({ handle, onDragStart, sx }) => { return ( onDragStart(handle, e)} onTouchStart={(e) => onDragStart(handle, e)} sx={sx} /> ); }; export default CropTool;