function LuxuryImageViewer({imgs,index,onIndexChange,onClose,alt}){ const MAX_ZOOM=4; const [zoom,setZoom]=React.useState(1); const [pan,setPan]=React.useState({x:0,y:0}); const [dragging,setDragging]=React.useState(false); const [sliderDragging,setSliderDragging]=React.useState(false); const [entered,setEntered]=React.useState(false); const stageRef=React.useRef(null); const dragStart=React.useRef(null); const pinchStart=React.useRef(null); const lastTap=React.useRef(0); const sliderTrackRef=React.useRef(null); const TRACK_H=180; React.useEffect(()=>{ const t=requestAnimationFrame(()=>setEntered(true)); return ()=>cancelAnimationFrame(t); },[]); React.useEffect(()=>{ [index-1,index+1].forEach(i=>{ if(i>=0&&i{ setZoom(1); setPan({x:0,y:0}); },[index]); function clampPan(z,p){ const el=stageRef.current; if(!el)return p; const maxX=(el.clientWidth*(z-1))/2; const maxY=(el.clientHeight*(z-1))/2; return {x:Math.max(-maxX,Math.min(maxX,p.x)),y:Math.max(-maxY,Math.min(maxY,p.y))}; } function setZoomClamped(z){ const nz=Math.max(1,Math.min(MAX_ZOOM,z)); setZoom(nz); setPan(p=>clampPan(nz,nz===1?{x:0,y:0}:p)); } React.useEffect(()=>{ function onKey(e){ if(e.key==='Escape')onClose(); else if(e.key==='ArrowLeft')onIndexChange((index-1+imgs.length)%imgs.length); else if(e.key==='ArrowRight')onIndexChange((index+1)%imgs.length); } document.addEventListener('keydown',onKey); return ()=>document.removeEventListener('keydown',onKey); },[index,imgs.length]); function onWheel(e){ e.preventDefault(); setZoomClamped(zoom - e.deltaY*0.0025); } function onPointerDown(e){ if(zoom<=1)return; setDragging(true); dragStart.current={x:e.clientX,y:e.clientY,pan}; e.currentTarget.setPointerCapture&&e.currentTarget.setPointerCapture(e.pointerId); } function onPointerMove(e){ if(!dragging||!dragStart.current)return; const dx=e.clientX-dragStart.current.x; const dy=e.clientY-dragStart.current.y; setPan(clampPan(zoom,{x:dragStart.current.pan.x+dx,y:dragStart.current.pan.y+dy})); } function onPointerUp(){ setDragging(false); dragStart.current=null; } function onDoubleClick(e){ if(zoom>1){setZoomClamped(1);return;} const r=stageRef.current.getBoundingClientRect(); const cx=(e.clientX-r.left-r.width/2); const cy=(e.clientY-r.top-r.height/2); setZoom(2.5); setPan(clampPan(2.5,{x:-cx*1.5,y:-cy*1.5})); } function onTouchStart(e){ if(e.touches.length===2){ const [a,b]=e.touches; pinchStart.current={dist:Math.hypot(a.clientX-b.clientX,a.clientY-b.clientY),zoom}; }else if(e.touches.length===1){ const now=Date.now(); if(now-lastTap.current<300){ onDoubleClick({clientX:e.touches[0].clientX,clientY:e.touches[0].clientY,currentTarget:stageRef.current}); } lastTap.current=now; if(zoom>1){ dragStart.current={x:e.touches[0].clientX,y:e.touches[0].clientY,pan}; setDragging(true); } } } function onTouchMove(e){ if(e.touches.length===2&&pinchStart.current){ const [a,b]=e.touches; const dist=Math.hypot(a.clientX-b.clientX,a.clientY-b.clientY); setZoomClamped(pinchStart.current.zoom*(dist/pinchStart.current.dist)); }else if(e.touches.length===1&&dragging&&dragStart.current){ const dx=e.touches[0].clientX-dragStart.current.x; const dy=e.touches[0].clientY-dragStart.current.y; setPan(clampPan(zoom,{x:dragStart.current.pan.x+dx,y:dragStart.current.pan.y+dy})); } } function onTouchEnd(){ setDragging(false); dragStart.current=null; pinchStart.current=null; } function sliderPosFromZoom(z){ return (1-(z-1)/(MAX_ZOOM-1))*TRACK_H; } function zoomFromSliderPos(y){ const clamped=Math.max(0,Math.min(TRACK_H,y)); return MAX_ZOOM-(clamped/TRACK_H)*(MAX_ZOOM-1); } function onSliderPointerDown(e){ setSliderDragging(true); updateFromSliderEvent(e); } function updateFromSliderEvent(e){ const r=sliderTrackRef.current.getBoundingClientRect(); setZoomClamped(zoomFromSliderPos(e.clientY-r.top)); } React.useEffect(()=>{ if(!sliderDragging)return; function move(e){ updateFromSliderEvent(e); } function up(){ setSliderDragging(false); } document.addEventListener('pointermove',move); document.addEventListener('pointerup',up); return ()=>{ document.removeEventListener('pointermove',move); document.removeEventListener('pointerup',up); }; },[sliderDragging,zoom]); return (
{ if(e.target===e.currentTarget)onClose(); }} style={{position:'fixed',inset:0,zIndex:1000,background:'var(--ivory)',opacity:entered?1:0,transition:'opacity 320ms var(--ease-out)',display:'flex',alignItems:'center',justifyContent:'center'}}>
+
{alt||''}1?(dragging?'grabbing':'grab'):'zoom-in',transform:`translate(${pan.x}px,${pan.y}px) scale(${zoom})`,transition:dragging||sliderDragging?'none':'transform 250ms var(--ease-out)',userSelect:'none'}}/>
{imgs.length>1&&(
{imgs.map((im,i)=>( ))}
)}
); } window.LuxuryImageViewer=LuxuryImageViewer;