import { ArrowLeftRight, Download, Grid3x3 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Crop } from "react-image-crop"; import { ProgressCard } from "@/components/common/progress-card"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; const ASPECT_PRESETS = [ { label: "Free", value: undefined as number | undefined }, { label: "1:1", value: 1 }, { label: "4:3", value: 4 / 3 }, { label: "3:2", value: 3 / 2 }, { label: "16:9", value: 16 / 9 }, { label: "2:3", value: 2 / 3 }, { label: "4:5", value: 4 / 5 }, { label: "9:16", value: 9 / 16 }, ]; export interface CropSettingsProps { cropState: { crop: Crop; aspect: number | undefined; showGrid: boolean; imgDimensions: { width: number; height: number } | null; }; onCropChange: (crop: Crop) => void; onAspectChange: (aspect: number | undefined) => void; onGridToggle: (show: boolean) => void; } export function CropSettings({ cropState, onCropChange, onAspectChange, onGridToggle, }: CropSettingsProps) { const { files } = useFileStore(); const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("crop"); const { crop, aspect, showGrid, imgDimensions } = cropState; const [customMode, setCustomMode] = useState(false); const [customW, setCustomW] = useState("3"); const [customH, setCustomH] = useState("2"); // Convert percentage crop to pixel values const toPixels = useCallback( (c: Crop) => { if (!imgDimensions) return { left: 0, top: 0, width: 0, height: 0 }; return { left: Math.round((c.x / 100) * imgDimensions.width), top: Math.round((c.y / 100) * imgDimensions.height), width: Math.round((c.width / 100) * imgDimensions.width), height: Math.round((c.height / 100) * imgDimensions.height), }; }, [imgDimensions], ); // Convert pixel value change back to percentage crop const handlePixelChange = useCallback( (field: "left" | "top" | "width" | "height", value: number) => { if (!imgDimensions) return; const newCrop = { ...crop }; if (field === "left") { newCrop.x = Math.max(0, Math.min((value / imgDimensions.width) * 100, 100 - newCrop.width)); } else if (field === "top") { newCrop.y = Math.max( 0, Math.min((value / imgDimensions.height) * 100, 100 - newCrop.height), ); } else if (field === "width") { const pct = Math.max(0, Math.min((value / imgDimensions.width) * 100, 100 - newCrop.x)); newCrop.width = pct; if (aspect) { newCrop.height = Math.min( (pct / 100) * imgDimensions.width * (1 / aspect) * (100 / imgDimensions.height), 100 - newCrop.y, ); newCrop.y = Math.max(0, Math.min(newCrop.y, 100 - newCrop.height)); } } else if (field === "height") { const pct = Math.max(0, Math.min((value / imgDimensions.height) * 100, 100 - newCrop.y)); newCrop.height = pct; if (aspect) { newCrop.width = Math.min( (pct / 100) * imgDimensions.height * aspect * (100 / imgDimensions.width), 100 - newCrop.x, ); newCrop.x = Math.max(0, Math.min(newCrop.x, 100 - newCrop.width)); } } onCropChange(newCrop); }, [crop, imgDimensions, aspect, onCropChange], ); const handleAspectSelect = useCallback( (value: number | undefined) => { setCustomMode(false); onAspectChange(value); // When selecting an aspect ratio, adjust the current crop to match if (value && imgDimensions) { const imgAspect = imgDimensions.width / imgDimensions.height; let newWidth: number; let newHeight: number; if (value > imgAspect) { // Desired ratio is wider than image — use full width, shrink height newWidth = 100; newHeight = (imgDimensions.width / value / imgDimensions.height) * 100; } else { // Desired ratio is taller than image — use full height, shrink width newHeight = 100; newWidth = ((imgDimensions.height * value) / imgDimensions.width) * 100; } onCropChange({ unit: "%", x: (100 - newWidth) / 2, y: (100 - newHeight) / 2, width: newWidth, height: newHeight, }); } }, [onAspectChange, onCropChange, imgDimensions], ); const applyCustomAspect = useCallback( (w: number, h: number) => { if (w > 0 && h > 0) { const value = w / h; onAspectChange(value); if (imgDimensions) { const imgAspect = imgDimensions.width / imgDimensions.height; let newWidth: number; let newHeight: number; if (value > imgAspect) { newWidth = 100; newHeight = (imgDimensions.width / value / imgDimensions.height) * 100; } else { newHeight = 100; newWidth = ((imgDimensions.height * value) / imgDimensions.width) * 100; } onCropChange({ unit: "%", x: (100 - newWidth) / 2, y: (100 - newHeight) / 2, width: newWidth, height: newHeight, }); } } }, [onAspectChange, onCropChange, imgDimensions], ); const handleCustomSelect = useCallback(() => { setCustomMode(true); const w = Number(customW); const h = Number(customH); applyCustomAspect(w, h); }, [customW, customH, applyCustomAspect]); const handleSwapAspect = useCallback(() => { if (aspect) { handleAspectSelect(1 / aspect); } }, [aspect, handleAspectSelect]); const pixels = toPixels(crop); const handleProcess = () => { if (files.length > 1) { // Send percentage-based crop so the server adapts to each image's dimensions const settings = { left: crop.x, top: crop.y, width: Math.max(0.1, crop.width), height: Math.max(0.1, crop.height), unit: "percent" as const, }; processAllFiles(files, settings); } else { const settings = { left: pixels.left, top: pixels.top, width: Math.max(1, pixels.width), height: Math.max(1, pixels.height), }; processFiles(files, settings); } }; const hasFile = files.length > 0; const hasSize = pixels.width > 0 && pixels.height > 0; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (hasFile && hasSize && !processing) handleProcess(); }; // Find which preset label matches the current aspect const activePresetLabel = ASPECT_PRESETS.find((p) => { if (p.value === undefined && aspect === undefined) return true; if (p.value !== undefined && aspect !== undefined) { return Math.abs(p.value - aspect) < 0.01; } return false; })?.label; return (
); } // ── Pipeline-only crop controls (numeric inputs, no canvas) ────────── export interface CropControlsProps { settings?: Record