import { Download } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useTranslation } from "@/contexts/i18n-context"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; const PREVIEW_D = 200; // circle diameter in the inline preview, px export function CircleCropSettings() { const { t } = useTranslation(); const { files } = useFileStore(); const entry = useFileStore((s) => s.entries[s.selectedIndex]); const blobUrl = entry?.blobUrl; const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("circle-crop"); const [dims, setDims] = useState<{ w: number; h: number } | null>(null); const [zoom, setZoom] = useState(1); const [offsetX, setOffsetX] = useState(0.5); const [offsetY, setOffsetY] = useState(0.5); const [borderWidth, setBorderWidth] = useState(0); const [borderColor, setBorderColor] = useState("#ffffff"); const [bgMode, setBgMode] = useState<"transparent" | "color">("transparent"); const [bgColor, setBgColor] = useState("#ffffff"); const [outputSize, setOutputSize] = useState(""); // Natural image dimensions, for accurate framing math. useEffect(() => { if (!blobUrl) { setDims(null); return; } const img = new Image(); img.onload = () => setDims({ w: img.naturalWidth, h: img.naturalHeight }); img.src = blobUrl; }, [blobUrl]); // Preview geometry (mirrors the backend region math). const W = dims?.w ?? 1; const H = dims?.h ?? 1; const d = Math.max(1, Math.min(W, H) / zoom); const scale = PREVIEW_D / d; const left = (W - d) * offsetX; const top = (H - d) * offsetY; const bwPx = borderWidth * scale; // Drag-to-pan the circle. const drag = useRef<{ px: number; py: number; ox: number; oy: number } | null>(null); const onPointerDown = (e: React.PointerEvent) => { if (!dims) return; e.currentTarget.setPointerCapture(e.pointerId); drag.current = { px: e.clientX, py: e.clientY, ox: offsetX, oy: offsetY }; }; const onPointerMove = (e: React.PointerEvent) => { if (!drag.current || !dims) return; const dx = e.clientX - drag.current.px; const dy = e.clientY - drag.current.py; const rangeX = W - d; const rangeY = H - d; if (rangeX > 0) { const nl = Math.min(Math.max(drag.current.ox * rangeX - dx / scale, 0), rangeX); setOffsetX(nl / rangeX); } if (rangeY > 0) { const nt = Math.min(Math.max(drag.current.oy * rangeY - dy / scale, 0), rangeY); setOffsetY(nt / rangeY); } }; const onPointerUp = () => { drag.current = null; }; const handleProcess = () => { const settings: Record = { zoom, offsetX, offsetY, borderWidth, borderColor, background: bgMode === "transparent" ? "transparent" : bgColor, }; const sz = Number.parseInt(outputSize, 10); if (!Number.isNaN(sz) && sz >= 16) settings.outputSize = sz; if (files.length > 1) processAllFiles(files, settings); else processFiles(files, settings); }; const hasFile = files.length > 0; return (
{/* Live framing preview */} {hasFile && blobUrl && (
0 ? borderColor : "transparent", }} >
{/* checkerboard hint for transparency */} {bgMode === "transparent" && (
)}
)} {hasFile && (

Drag the preview to reposition

)} {/* Zoom */}
Zoom {zoom.toFixed(1)}x
setZoom(Number(e.target.value))} className="w-full mt-1" />
{/* Border */}
Border {borderWidth}px
setBorderWidth(Number(e.target.value))} className="flex-1 min-w-0" /> setBorderColor(e.target.value)} aria-label="Border color" className="h-7 w-9 shrink-0 rounded border border-border bg-background" />
{/* Background */}
Background
{bgMode === "color" && ( setBgColor(e.target.value)} aria-label="Background color" className="h-7 w-9 shrink-0 rounded border border-border bg-background" /> )}
{/* Output size */}
setOutputSize(e.target.value)} placeholder="Original" className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" />
{error &&

{error}

} {processing ? ( ) : ( )} {downloadUrl && ( {t.common.download} )}
); }