import React, { useRef, useEffect, useState } from 'react'; import { X } from 'lucide-react'; import { useCropHandles, type CropBounds, type HandleType } from '@/hooks/useCropHandles'; interface ManualCropUIProps { imageUrl: string; onCropChange: (bounds: CropBounds | null) => void; imageDimensions?: { width: number; height: number }; initialCrop?: CropBounds; } const HANDLE_SIZE = 12; const HANDLE_TYPES: HandleType[] = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left', ]; export default function ManualCropUI({ imageUrl, onCropChange, imageDimensions, initialCrop, }: ManualCropUIProps) { const imageRef = useRef(null); const containerRef = useRef(null); const [displayDimensions, setDisplayDimensions] = useState<{ width: number; height: number } | null>(null); const [error, setError] = useState(null); const actualDimensions = imageDimensions || displayDimensions; const { crop, setCrop, startDrag, moveDrag, endDrag, resetCrop, isDragging } = useCropHandles( actualDimensions ? { imageDimensions: actualDimensions, initialCrop, minSize: 100, } : { imageDimensions: { width: 1, height: 1 }, initialCrop, minSize: 100 } ); // Measure image dimensions on load const handleImageLoad = (e: React.SyntheticEvent) => { const img = e.currentTarget; const width = img.naturalWidth || img.width; const height = img.naturalHeight || img.height; if (width && height) { setDisplayDimensions({ width, height }); setError(null); } }; const handleImageError = () => { setError('Failed to load image'); }; // Emit crop changes to parent useEffect(() => { onCropChange(crop); }, [crop, onCropChange]); if (error) { return (

{error}

); } // If no dimensions yet, show minimal loading state with hidden image if (!actualDimensions) { return (
Photo preview
Loading image...
); } const containerWidth = containerRef.current?.clientWidth || 400; const scale = containerWidth / actualDimensions.width; const displayWidth = actualDimensions.width * scale; const displayHeight = actualDimensions.height * scale; const getHandlePosition = ( handleType: HandleType ): { left: string; top: string; transform: string } => { if (!crop) { return { left: '0', top: '0', transform: 'translate(0, 0)' }; } const x = crop.x * scale; const y = crop.y * scale; const w = crop.width * scale; const h = crop.height * scale; const offsets = { 'top-left': { left: x, top: y }, top: { left: x + w / 2, top: y }, 'top-right': { left: x + w, top: y }, right: { left: x + w, top: y + h / 2 }, 'bottom-right': { left: x + w, top: y + h }, bottom: { left: x + w / 2, top: y + h }, 'bottom-left': { left: x, top: y + h }, left: { left: x, top: y + h / 2 }, }; const pos = offsets[handleType]; return { left: `${pos.left}px`, top: `${pos.top}px`, transform: 'translate(-50%, -50%)', }; }; const handleMouseDown = (handleType: HandleType) => (e: React.MouseEvent) => { e.preventDefault(); if (!containerRef.current || !crop) return; const rect = containerRef.current.getBoundingClientRect(); const startX = (e.clientX - rect.left) / scale; const startY = (e.clientY - rect.top) / scale; startDrag(handleType, startX, startY); }; const handleTouchStart = (handleType: HandleType) => (e: React.TouchEvent) => { e.preventDefault(); if (!containerRef.current || !crop || e.touches.length === 0) return; const rect = containerRef.current.getBoundingClientRect(); const touch = e.touches[0]; const startX = (touch.clientX - rect.left) / scale; const startY = (touch.clientY - rect.top) / scale; startDrag(handleType, startX, startY); }; // Global mouse/touch move and end listeners useEffect(() => { if (!isDragging) return; const handleMouseMove = (e: MouseEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const currentX = (e.clientX - rect.left) / scale; const currentY = (e.clientY - rect.top) / scale; moveDrag(currentX, currentY); }; const handleTouchMove = (e: TouchEvent) => { if (!containerRef.current || e.touches.length === 0) return; const rect = containerRef.current.getBoundingClientRect(); const touch = e.touches[0]; const currentX = (touch.clientX - rect.left) / scale; const currentY = (touch.clientY - rect.top) / scale; moveDrag(currentX, currentY); }; const handleEnd = () => { endDrag(); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleEnd); document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleEnd); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleEnd); }; }, [isDragging, scale, moveDrag, endDrag]); return (
{/* Image container with crop preview */}
{/* Image */} Photo preview {/* Semi-transparent overlay outside crop box */} {crop && (
{/* Top overlay */}
{/* Bottom overlay */}
{/* Left overlay */}
{/* Right overlay */}
{/* Crop bounding box */}
{/* Handles */} {HANDLE_TYPES.map((handleType) => (
)}
{/* Controls */}
{crop && ( )} {!crop && (
Drag handles to adjust crop area
)}
{/* Crop bounds display (debug) */} {crop && (
Crop: {Math.round(crop.x)}, {Math.round(crop.y)} | Size: {Math.round(crop.width)} x{' '} {Math.round(crop.height)}
)}
); }