diff --git a/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx b/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx index baaea09..b1cbc8e 100644 --- a/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx +++ b/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx @@ -40,7 +40,10 @@ function ModalContent({ file, onConfirm, onCancel }: FilePreviewModalProps & { f const [sending, setSending] = useState(false); const [objectUrl, setObjectUrl] = useState(null); const [zoomed, setZoomed] = useState(false); + // Posição do mouse como % dentro da área da imagem (para o zoom seguir o cursor) + const [mouseOrigin, setMouseOrigin] = useState({ x: 50, y: 50 }); const captionRef = useRef(null); + const previewAreaRef = useRef(null); const isImage = file.type.startsWith('image/'); const isVideo = file.type.startsWith('video/'); @@ -51,6 +54,7 @@ function ModalContent({ file, onConfirm, onCancel }: FilePreviewModalProps & { f setCaption(''); setSending(false); setZoomed(false); + setMouseOrigin({ x: 50, y: 50 }); setTimeout(() => captionRef.current?.focus(), 250); return () => URL.revokeObjectURL(url); }, [file]); @@ -74,6 +78,17 @@ function ModalContent({ file, onConfirm, onCancel }: FilePreviewModalProps & { f if (e.key === 'Enter') { e.preventDefault(); handleSend(); } }; + // Atualiza a origem do zoom conforme o mouse se move na área de preview + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!isImage) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + setMouseOrigin({ x, y }); + }, [isImage]); + + const toggleZoom = useCallback(() => setZoomed(z => !z), []); + return ( {/* ── Top bar ── */}
- - + {/* Info do arquivo — lado esquerdo */}

{file.name}

{getFileLabel(file.type)} · {formatFileSize(file.size)}

- {isImage && ( + {/* Botões — lado direito: zoom (se imagem) → fechar */} +
+ {isImage && ( + + )} - )} +
{/* ── Preview area ── */}
isImage && setZoomed(z => !z)} + ref={previewAreaRef} + className="flex-1 flex items-center justify-center min-h-0 overflow-hidden p-4" + onMouseMove={handleMouseMove} + onClick={isImage ? toggleZoom : undefined} style={{ cursor: isImage ? (zoomed ? 'zoom-out' : 'zoom-in') : 'default' }} > {isImage && objectUrl && ( @@ -122,12 +143,26 @@ function ModalContent({ file, onConfirm, onCancel }: FilePreviewModalProps & { f initial={{ scale: 0.94, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ duration: 0.22 }} - style={zoomed - ? { maxWidth: 'none', maxHeight: 'none', width: 'auto', height: 'auto', cursor: 'zoom-out' } - : { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', cursor: 'zoom-in' } - } - className="rounded-lg shadow-2xl select-none" draggable={false} + className="select-none rounded-lg shadow-2xl" + style={zoomed ? { + // Zoom 2.5× centrado no cursor + transform: `scale(2.5)`, + transformOrigin: `${mouseOrigin.x}% ${mouseOrigin.y}%`, + transition: 'transform-origin 0s', // origin segue o mouse instantaneamente + maxWidth: '100%', + maxHeight: 'calc(100vh - 160px)', + objectFit: 'contain', + cursor: 'zoom-out', + } : { + maxWidth: '100%', + maxHeight: 'calc(100vh - 160px)', + objectFit: 'contain', + cursor: 'zoom-in', + transition: 'transform 0.2s ease', + transform: 'scale(1)', + transformOrigin: 'center center', + }} /> )}