import { useState, useCallback } from 'react'; export interface CropBounds { x: number; y: number; width: number; height: number; } export type HandleType = | 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left'; interface UseCropHandlesOptions { imageDimensions: { width: number; height: number }; initialCrop?: CropBounds; minSize?: number; } interface UseCropHandlesReturn { crop: CropBounds | null; setCrop: (bounds: CropBounds | null) => void; startDrag: (handleType: HandleType, startX: number, startY: number) => void; moveDrag: (currentX: number, currentY: number) => void; endDrag: () => void; resetCrop: () => void; isDragging: boolean; } const MIN_SIZE_DEFAULT = 100; export function useCropHandles({ imageDimensions, initialCrop, minSize = MIN_SIZE_DEFAULT, }: UseCropHandlesOptions): UseCropHandlesReturn { // Constrain initial crop if provided const constrainInitialCrop = (bounds: CropBounds | undefined): CropBounds | null => { if (!bounds) return null; const { width: imgW, height: imgH } = imageDimensions; let { x, y, width, height } = bounds; width = Math.max(minSize, width); height = Math.max(minSize, height); x = Math.max(0, Math.min(x, imgW - width)); y = Math.max(0, Math.min(y, imgH - height)); width = Math.min(width, imgW - x); height = Math.min(height, imgH - y); return { x, y, width, height }; }; const [crop, setCropState] = useState( constrainInitialCrop(initialCrop) ); const [isDragging, setIsDragging] = useState(false); const [dragState, setDragState] = useState<{ handleType: HandleType; startX: number; startY: number; startCrop: CropBounds; } | null>(null); const constrainBounds = useCallback( (bounds: CropBounds): CropBounds => { const { width: imgW, height: imgH } = imageDimensions; // Enforce minimum size let { x, y, width, height } = bounds; width = Math.max(minSize, width); height = Math.max(minSize, height); // Constrain within image bounds x = Math.max(0, Math.min(x, imgW - width)); y = Math.max(0, Math.min(y, imgH - height)); // Ensure bounds don't exceed image dimensions width = Math.min(width, imgW - x); height = Math.min(height, imgH - y); return { x, y, width, height }; }, [imageDimensions, minSize] ); const startDrag = useCallback( (handleType: HandleType, startX: number, startY: number) => { if (!crop) return; setIsDragging(true); setDragState({ handleType, startX, startY, startCrop: { ...crop }, }); }, [crop] ); const moveDrag = useCallback( (currentX: number, currentY: number) => { if (!dragState || !crop) return; const deltaX = currentX - dragState.startX; const deltaY = currentY - dragState.startY; const { x, y, width, height } = dragState.startCrop; const { width: imgW, height: imgH } = imageDimensions; let newBounds: CropBounds = { x, y, width, height }; switch (dragState.handleType) { case 'top-left': newBounds = { x: x + deltaX, y: y + deltaY, width: width - deltaX, height: height - deltaY, }; break; case 'top': newBounds = { x, y: y + deltaY, width, height: height - deltaY, }; break; case 'top-right': newBounds = { x, y: y + deltaY, width: width + deltaX, height: height - deltaY, }; break; case 'right': newBounds = { x, y, width: width + deltaX, height, }; break; case 'bottom-right': newBounds = { x, y, width: width + deltaX, height: height + deltaY, }; break; case 'bottom': newBounds = { x, y, width, height: height + deltaY, }; break; case 'bottom-left': newBounds = { x: x + deltaX, y, width: width - deltaX, height: height + deltaY, }; break; case 'left': newBounds = { x: x + deltaX, y, width: width - deltaX, height, }; break; } newBounds = constrainBounds(newBounds); setCropState(newBounds); }, [dragState, crop, constrainBounds, imageDimensions] ); const endDrag = useCallback(() => { setIsDragging(false); setDragState(null); }, []); const resetCrop = useCallback(() => { setCropState(null); }, []); const setCrop = useCallback( (bounds: CropBounds | null) => { if (!bounds) { setCropState(null); return; } const constrained = constrainBounds(bounds); setCropState(constrained); }, [constrainBounds] ); return { crop, setCrop, startDrag, moveDrag, endDrag, resetCrop, isDragging, }; }