import { useRef, useState, useCallback, useEffect, type PointerEvent } from "react"; interface BeforeAfterSliderProps { /** URL or data URL of original image. */ beforeSrc: string; /** URL or data URL of processed image. */ afterSrc: string; /** Original file size in bytes. */ beforeSize?: number; /** Processed file size in bytes. */ afterSize?: number; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } /** * Before/After image comparison slider. * * Shows two images overlapping with a draggable vertical divider. * The "before" image is on the left, "after" on the right. * Supports mouse and touch interaction via pointer events. */ export function BeforeAfterSlider({ beforeSrc, afterSrc, beforeSize, afterSize, }: BeforeAfterSliderProps) { const containerRef = useRef(null); const [position, setPosition] = useState(50); // percentage 0-100 const [isDragging, setIsDragging] = useState(false); const updatePosition = useCallback( (clientX: number) => { const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const x = clientX - rect.left; const pct = Math.max(0, Math.min(100, (x / rect.width) * 100)); setPosition(pct); }, [], ); const handlePointerDown = useCallback( (e: PointerEvent) => { e.preventDefault(); setIsDragging(true); (e.target as HTMLElement).setPointerCapture(e.pointerId); updatePosition(e.clientX); }, [updatePosition], ); const handlePointerMove = useCallback( (e: PointerEvent) => { if (!isDragging) return; updatePosition(e.clientX); }, [isDragging, updatePosition], ); const handlePointerUp = useCallback(() => { setIsDragging(false); }, []); // Prevent default drag behavior on images useEffect(() => { const container = containerRef.current; if (!container) return; const preventDrag = (e: Event) => e.preventDefault(); container.addEventListener("dragstart", preventDrag); return () => container.removeEventListener("dragstart", preventDrag); }, []); const savingsPercent = beforeSize && afterSize && beforeSize > 0 ? ((1 - afterSize / beforeSize) * 100).toFixed(1) : null; return (
{/* Slider container */}
{/* Before image (full width, bottom layer) */} Original {/* After image (clipped, top layer) — checkerboard background shows transparency */}
Processed
{/* Divider line */}
{/* Handle grip */}
{/* Labels */}
Original
Processed
{/* Size comparison badges */} {beforeSize != null && afterSize != null && (
Original: {formatSize(beforeSize)} Processed: {formatSize(afterSize)} {savingsPercent !== null && Number(savingsPercent) > 0 && ( ({savingsPercent}% smaller) )} {savingsPercent !== null && Number(savingsPercent) < 0 && ( ({Math.abs(Number(savingsPercent))}% larger) )}
)}
); }