"use client"; import Image from "next/image"; import { Maximize2, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react"; import { MouseEvent, PointerEvent, WheelEvent, useRef, useState } from "react"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; interface Offset { x: number; y: number } interface DragState { pointerId: number; startX: number; startY: number; originX: number; originY: number } interface HoverZoomOrigin { x: number; y: number } // Zoom automatico al pasar el cursor (estilo lupa clasico de e-commerce) // sobre la imagen principal -- solo mouse (mousemove no dispara de forma // continua en touch, asi que en mobile simplemente no aparece el efecto, // sin romper nada). Independiente del zoom interactivo del modal // (click -> Dialog con scale/drag), que sigue igual. // // El overlay de hover usa background-image (no transform:scale sobre el // ) a proposito: escalar la imagen ya recortada por // object-cover nunca iba a revelar las partes recortadas (ej. la cabeza // de una figura de accion en una foto vertical) -- el recorte pasa antes // del zoom, en el propio render. Con background-size en un solo valor // (ancho%, alto en "auto") se preserva el aspect ratio real de la foto // completa, y background-position 0%-100% permite recorrer TODA la // imagen con el cursor, no solo la porcion que object-cover dejaba // visible en reposo. const HOVER_ZOOM_SCALE = 2.2; export function ProductGallery({ images, name }: { images: string[]; name: string }) { const [selected, setSelected] = useState(images[0] ?? ""); const [open, setOpen] = useState(false); const [scale, setScale] = useState(1); const [offset, setOffset] = useState({ x: 0, y: 0 }); const [hoverZoom, setHoverZoom] = useState(null); const drag = useRef(null); const trackHoverZoom = (event: MouseEvent) => { const rect = event.currentTarget.getBoundingClientRect(); setHoverZoom({ x: ((event.clientX - rect.left) / rect.width) * 100, y: ((event.clientY - rect.top) / rect.height) * 100, }); }; const applyScale = (next: number) => { const value = Math.min(3, Math.max(1, Number(next.toFixed(2)))); setScale(value); if (value === 1) setOffset({ x: 0, y: 0 }); }; const reset = () => { setScale(1); setOffset({ x: 0, y: 0 }); }; const changeOpen = (value: boolean) => { setOpen(value); if (!value) reset(); }; const wheel = (event: WheelEvent) => { event.preventDefault(); applyScale(scale + (event.deltaY < 0 ? .2 : -.2)); }; const pointerDown = (event: PointerEvent) => { if (scale <= 1) return; event.currentTarget.setPointerCapture(event.pointerId); drag.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, originX: offset.x, originY: offset.y }; }; const pointerMove = (event: PointerEvent) => { if (!drag.current || drag.current.pointerId !== event.pointerId) return; setOffset({ x: drag.current.originX + event.clientX - drag.current.startX, y: drag.current.originY + event.clientY - drag.current.startY }); }; const pointerEnd = (event: PointerEvent) => { if (drag.current?.pointerId === event.pointerId) drag.current = null; }; return (
{images.map((image) => )}
applyScale(scale > 1 ? 1 : 2)} style={{ touchAction: "none", cursor: scale > 1 ? "grab" : "zoom-in" }}>
{name}
Rueda o botones para acercar ยท Arrastra para mover
applyScale(Number(event.target.value))} className="w-44 accent-fuchsia-500 sm:w-64" /> {Math.round(scale * 100)}%
); }