zoomora 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/zoomora.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Zoomora Lightbox Plugin v1.0.0
2
+ * Zoomora Lightbox Plugin v1.2.0
3
3
  * A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
4
4
  *
5
- * Copyright (c) 2025 Faruk Ahmed (FrontTheme)
5
+ * Copyright (c) 2026 Faruk Ahmed (FrontTheme)
6
6
  * Licensed under MIT License
7
7
  */
8
8
 
@@ -46,6 +46,7 @@ import './styles/zoomora.scss';
46
46
  maxZoomScale: 3, // Maximum zoom scale
47
47
  zoomStep: 0.1, // Zoom step for scroll wheel
48
48
  animationDuration: 300, // Animation duration in ms
49
+ closeOnBackgroundClick: true, // Close lightbox when clicking on background
49
50
  onOpen: null, // Callback when lightbox opens
50
51
  onClose: null, // Callback when lightbox closes
51
52
  onNext: null, // Callback when navigating to next item
@@ -85,6 +86,8 @@ import './styles/zoomora.scss';
85
86
  this.isControlsVisible = true;
86
87
  this.idleTimer = null;
87
88
 
89
+ this.lastDragTime = 0;
90
+
88
91
  // Bound methods for event listeners
89
92
  this.boundMethods = {
90
93
  handleDocumentClick: this.handleDocumentClick.bind(this),
@@ -147,6 +150,13 @@ import './styles/zoomora.scss';
147
150
  return;
148
151
  }
149
152
 
153
+ // Build auto-hide button conditionally
154
+ const autoHideButtonHTML = this.options.showAutoHideToggle ? `
155
+ <button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
156
+ <i class="zoomorai-controls-off"></i>
157
+ </button>
158
+ ` : '';
159
+
150
160
  const zoomoraHTML = `
151
161
  <div class="zoomora" id="zoomoraLightbox">
152
162
  <div class="zoomora-container">
@@ -156,9 +166,7 @@ import './styles/zoomora.scss';
156
166
  <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
157
167
  <i class="zoomorai-thumbnails"></i>
158
168
  </button>
159
- <button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
160
- <i class="zoomorai-controls-off"></i>
161
- </button>
169
+ ${autoHideButtonHTML}
162
170
  <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
163
171
  <i class="zoomorai-zoom"></i>
164
172
  </button>
@@ -240,7 +248,7 @@ import './styles/zoomora.scss';
240
248
  // Lightbox click to close
241
249
  this.addEventListener(this.zoomora, 'click', this.boundMethods.handleZoomoraClick);
242
250
 
243
- // Content click for zoom toggle
251
+ // Content click for zoom toggle and background click
244
252
  this.addEventListener(this.content, 'click', this.boundMethods.handleContentClick);
245
253
 
246
254
  // Mouse drag events
@@ -254,13 +262,15 @@ import './styles/zoomora.scss';
254
262
  this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
255
263
  this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
256
264
 
257
- // Add auto-hide toggle button handler
258
- this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
265
+ // Add auto-hide toggle button handler (only if enabled)
266
+ if (this.options.showAutoHideToggle) {
267
+ this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
259
268
 
260
- // Add mouse/touch activity listeners for auto-hide
261
- this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
262
- this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
263
- this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
269
+ // Add mouse/touch activity listeners for auto-hide
270
+ this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
271
+ this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
272
+ this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
273
+ }
264
274
  }
265
275
 
266
276
  /**
@@ -335,16 +345,25 @@ import './styles/zoomora.scss';
335
345
  * Handle fullscreen change events
336
346
  */
337
347
  handleFullscreenChange() {
338
- const isInFullscreen = document.fullscreenElement ||
348
+ const isInFullscreen = !!(
349
+ document.fullscreenElement ||
339
350
  document.webkitFullscreenElement ||
340
351
  document.mozFullScreenElement ||
341
- document.msFullscreenElement;
352
+ document.msFullscreenElement
353
+ );
342
354
 
355
+ // Only update if state changed from fullscreen to not fullscreen
343
356
  if (!isInFullscreen && this.isFullscreen) {
344
357
  this.isFullscreen = false;
345
358
  this.zoomora.classList.remove('fullscreen');
346
359
  this.updateButtons();
347
360
  }
361
+ // Update if entering fullscreen
362
+ else if (isInFullscreen && !this.isFullscreen) {
363
+ this.isFullscreen = true;
364
+ this.zoomora.classList.add('fullscreen');
365
+ this.updateButtons();
366
+ }
348
367
  }
349
368
 
350
369
  /**
@@ -352,7 +371,7 @@ import './styles/zoomora.scss';
352
371
  * @param {Event} e - Click event
353
372
  */
354
373
  handleZoomoraClick(e) {
355
- if (e.target === this.zoomora) {
374
+ if (e.target === this.zoomora && this.options.closeOnBackgroundClick) {
356
375
  this.close();
357
376
  }
358
377
  }
@@ -362,9 +381,65 @@ import './styles/zoomora.scss';
362
381
  * @param {Event} e - Click event
363
382
  */
364
383
  handleContentClick(e) {
365
- const media = e.target.closest('.zoomora-media');
366
- if (media && !media.classList.contains('no-zoom') && !this.isDragging) {
367
- this.toggleZoom();
384
+ // Ignore click if we just finished dragging (within 200ms)
385
+ if (Date.now() - this.lastDragTime < 200) {
386
+ return;
387
+ }
388
+
389
+ // Don't close on header clicks
390
+ if (this.header && this.header.contains(e.target)) {
391
+ return;
392
+ }
393
+
394
+ // Don't close on button clicks
395
+ if (e.target.closest('button')) {
396
+ return;
397
+ }
398
+
399
+ // Check if click is directly on actual media element (img, video, iframe)
400
+ const mediaElement = e.target.closest('.zoomora-media, .zoomora-video, .zoomora-iframe');
401
+
402
+ if (mediaElement) {
403
+ // Only toggle zoom for images
404
+ if (mediaElement.classList.contains('zoomora-media') && !mediaElement.classList.contains('no-zoom')) {
405
+ this.toggleZoom();
406
+ }
407
+ return;
408
+ }
409
+
410
+ // Check if clicking inside video container but not on video itself
411
+ const videoContainer = e.target.closest('.zoomora-video-container');
412
+ if (videoContainer) {
413
+ // If clicking on the play button, don't close
414
+ if (e.target.closest('.zoomora-play-button')) {
415
+ return;
416
+ }
417
+ // If clicking on background area of video container, close
418
+ if (this.options.closeOnBackgroundClick) {
419
+ this.close();
420
+ }
421
+ return;
422
+ }
423
+
424
+ // Check if clicking on background elements (including when zoomed)
425
+ // This checks the actual target element, not just closest()
426
+ const clickedElement = e.target;
427
+ const isBackgroundElement =
428
+ clickedElement.classList.contains('zoomora-content') ||
429
+ clickedElement.classList.contains('zoomora-slide-container') ||
430
+ clickedElement.classList.contains('zoomora-slide') ||
431
+ clickedElement.classList.contains('zoomora-slides-container') ||
432
+ clickedElement.id === 'zoomoraContent' ||
433
+ clickedElement.id === 'zoomoraSlidesContainer';
434
+
435
+ if (isBackgroundElement && this.options.closeOnBackgroundClick) {
436
+ this.close();
437
+ return;
438
+ }
439
+
440
+ // If we reach here and no media was clicked, close
441
+ if (this.options.closeOnBackgroundClick) {
442
+ this.close();
368
443
  }
369
444
  }
370
445
 
@@ -668,16 +743,19 @@ import './styles/zoomora.scss';
668
743
  return;
669
744
  }
670
745
 
746
+ // Reset zoom state
747
+ this.isZoomed = false;
748
+ this.currentX = 0;
749
+ this.currentY = 0;
750
+ this.currentScale = 1;
751
+
671
752
  // Show loading spinner
672
753
  this.loading.style.display = 'block';
673
754
 
674
- // Clear content but keep the loading reference intact
675
- // Don't replace innerHTML, just clear the slides
676
755
  if (this.slidesContainer) {
677
756
  this.slidesContainer.innerHTML = '';
678
757
  }
679
758
 
680
- // Add slight delay for smooth transition
681
759
  setTimeout(() => {
682
760
  this.renderContent(src, type, item);
683
761
  }, 100);
@@ -953,6 +1031,58 @@ import './styles/zoomora.scss';
953
1031
  }
954
1032
  }
955
1033
 
1034
+ /**
1035
+ * Calculate zoom levels for an image
1036
+ * @param {HTMLImageElement} img - Image element
1037
+ * @returns {Object} Zoom configuration
1038
+ */
1039
+ calculateZoomLevels(img) {
1040
+ if (!img || !img.naturalWidth || !img.naturalHeight) {
1041
+ return {canZoom: false, levels: [], currentLevel: 0};
1042
+ }
1043
+
1044
+ // Get CONTAINER size (not current displayed image size)
1045
+ const container = this.content.getBoundingClientRect();
1046
+ const containerWidth = container.width;
1047
+ const containerHeight = container.height;
1048
+
1049
+ const naturalWidth = img.naturalWidth;
1050
+ const naturalHeight = img.naturalHeight;
1051
+
1052
+ // Calculate what the ORIGINAL fit-to-screen scale would be
1053
+ const scaleToFitWidth = containerWidth / naturalWidth;
1054
+ const scaleToFitHeight = containerHeight / naturalHeight;
1055
+ const fitScale = Math.min(scaleToFitWidth, scaleToFitHeight);
1056
+
1057
+ // If image naturally fits or is smaller than container, can't zoom
1058
+ if (fitScale >= 0.95) {
1059
+ return {canZoom: false, levels: [], currentLevel: 0};
1060
+ }
1061
+
1062
+ // Calculate zoom levels based on FIT scale (not current scale)
1063
+ const levels = [1]; // Start with fit-to-screen
1064
+
1065
+ // Add progressive levels: 1.5x, 2x, 3x
1066
+ if (fitScale * 1.5 <= 1) levels.push(1.5);
1067
+ if (fitScale * 2 <= 1) levels.push(2);
1068
+ if (fitScale * 3 <= 1) levels.push(3);
1069
+
1070
+ // Add actual size (100%) as final level
1071
+ const actualSizeScale = 1 / fitScale;
1072
+ if (actualSizeScale > levels[levels.length - 1]) {
1073
+ levels.push(actualSizeScale);
1074
+ }
1075
+
1076
+ return {
1077
+ canZoom: levels.length > 1,
1078
+ levels: levels,
1079
+ currentLevel: 0,
1080
+ baseScale: fitScale,
1081
+ naturalWidth: naturalWidth,
1082
+ naturalHeight: naturalHeight
1083
+ };
1084
+ }
1085
+
956
1086
  /**
957
1087
  * Update counter display
958
1088
  */
@@ -1071,29 +1201,70 @@ import './styles/zoomora.scss';
1071
1201
  */
1072
1202
  toggleZoom() {
1073
1203
  const media = this.content.querySelector('.zoomora-media');
1074
- if (!media || media.tagName !== 'IMG' || media.classList.contains('no-zoom')) return;
1204
+ if (!media || media.tagName !== 'IMG') return;
1075
1205
 
1076
- this.isZoomed = !this.isZoomed;
1077
- media.classList.toggle('zoomed', this.isZoomed);
1206
+ // Calculate zoom levels
1207
+ const zoomConfig = this.calculateZoomLevels(media);
1078
1208
 
1079
- if (this.isZoomed) {
1080
- const zoomRatio = parseFloat(media.dataset.zoomRatio) || 2;
1081
- let zoomScale = Math.min(zoomRatio, this.options.maxZoomScale);
1209
+ if (!zoomConfig.canZoom) {
1210
+ return;
1211
+ }
1082
1212
 
1083
- if (zoomRatio > 2.5) {
1084
- zoomScale = Math.min(zoomRatio * 0.7, 2.5);
1085
- }
1213
+ // Get or initialize current zoom level
1214
+ if (!media.dataset.zoomLevel) {
1215
+ media.dataset.zoomLevel = '0';
1216
+ }
1086
1217
 
1087
- media.style.transform = `scale(${zoomScale})`;
1088
- media.style.cursor = 'grab';
1089
- media.dataset.currentZoomScale = zoomScale;
1090
- this.currentScale = zoomScale;
1091
- } else {
1218
+ let currentLevel = parseInt(media.dataset.zoomLevel) || 0;
1219
+
1220
+ // Cycle to next level
1221
+ currentLevel++;
1222
+ if (currentLevel >= zoomConfig.levels.length) {
1223
+ // Reset to fit-to-screen
1224
+ currentLevel = 0;
1225
+ }
1226
+
1227
+ const targetScale = zoomConfig.levels[currentLevel];
1228
+ media.dataset.zoomLevel = currentLevel.toString();
1229
+
1230
+ if (currentLevel === 0) {
1231
+ // Back to fit-to-screen
1232
+ this.isZoomed = false;
1233
+ this.isDragging = false;
1234
+ media.classList.remove('dragging');
1092
1235
  media.style.transform = '';
1093
- media.style.cursor = '';
1236
+ media.style.cursor = 'pointer';
1237
+ media.classList.remove('zoomed');
1094
1238
  this.currentX = 0;
1095
1239
  this.currentY = 0;
1096
1240
  this.currentScale = 1;
1241
+ } else {
1242
+ // Zoom in - reset position to center
1243
+ this.isZoomed = true;
1244
+ this.currentScale = targetScale;
1245
+ this.currentX = 0; // Always reset to center on zoom change
1246
+ this.currentY = 0;
1247
+
1248
+ media.style.transform = `translate(0px, 0px) scale(${targetScale})`;
1249
+ media.style.cursor = 'grab';
1250
+ media.classList.add('zoomed');
1251
+ }
1252
+ }
1253
+
1254
+ /**
1255
+ * Update image transform based on current state
1256
+ * @param {HTMLElement} media - Media element
1257
+ */
1258
+ updateImageTransform(media) {
1259
+ if (!media) return;
1260
+
1261
+ if (this.currentScale === 1) {
1262
+ media.style.transform = '';
1263
+ } else {
1264
+ // IMPORTANT: translate in scaled space, so divide by scale
1265
+ const translateX = this.currentX / this.currentScale;
1266
+ const translateY = this.currentY / this.currentScale;
1267
+ media.style.transform = `scale(${this.currentScale}) translate(${translateX}px, ${translateY}px)`;
1097
1268
  }
1098
1269
  }
1099
1270
 
@@ -1113,21 +1284,50 @@ import './styles/zoomora.scss';
1113
1284
  */
1114
1285
  enterFullscreen() {
1115
1286
  const element = this.zoomora;
1116
- const requestFullscreen = element.requestFullscreen ||
1117
- element.webkitRequestFullscreen ||
1118
- element.mozRequestFullScreen ||
1119
- element.msRequestFullscreen;
1120
1287
 
1121
- if (requestFullscreen) {
1122
- requestFullscreen.call(element).then(() => {
1288
+ try {
1289
+ // Try standard API first
1290
+ if (element.requestFullscreen) {
1291
+ element.requestFullscreen()
1292
+ .then(() => {
1293
+ this.isFullscreen = true;
1294
+ this.zoomora.classList.add('fullscreen');
1295
+ this.updateButtons();
1296
+ })
1297
+ .catch(err => {
1298
+ console.warn('Zoomora: Fullscreen request failed:', err.message);
1299
+ this.fallbackFullscreen();
1300
+ });
1301
+ }
1302
+ // Try webkit (Safari, older Chrome)
1303
+ else if (element.webkitRequestFullscreen) {
1304
+ element.webkitRequestFullscreen();
1305
+ // Webkit doesn't return a promise, so set state immediately
1306
+ this.isFullscreen = true;
1307
+ this.zoomora.classList.add('fullscreen');
1308
+ this.updateButtons();
1309
+ }
1310
+ // Try moz (Firefox)
1311
+ else if (element.mozRequestFullScreen) {
1312
+ element.mozRequestFullScreen();
1123
1313
  this.isFullscreen = true;
1124
1314
  this.zoomora.classList.add('fullscreen');
1125
1315
  this.updateButtons();
1126
- }).catch(err => {
1127
- console.warn('Zoomora: Could not enter fullscreen:', err);
1316
+ }
1317
+ // Try ms (IE11)
1318
+ else if (element.msRequestFullscreen) {
1319
+ element.msRequestFullscreen();
1320
+ this.isFullscreen = true;
1321
+ this.zoomora.classList.add('fullscreen');
1322
+ this.updateButtons();
1323
+ }
1324
+ // Fallback to CSS fullscreen
1325
+ else {
1326
+ console.warn('Zoomora: Fullscreen API not supported, using CSS fallback');
1128
1327
  this.fallbackFullscreen();
1129
- });
1130
- } else {
1328
+ }
1329
+ } catch (error) {
1330
+ console.error('Zoomora: Error entering fullscreen:', error);
1131
1331
  this.fallbackFullscreen();
1132
1332
  }
1133
1333
  }
@@ -1136,30 +1336,61 @@ import './styles/zoomora.scss';
1136
1336
  * Exit fullscreen mode
1137
1337
  */
1138
1338
  exitFullscreen() {
1139
- const isInFullscreen = document.fullscreenElement ||
1339
+ // First check if we're actually in fullscreen
1340
+ const isInFullscreen = !!(
1341
+ document.fullscreenElement ||
1140
1342
  document.webkitFullscreenElement ||
1141
1343
  document.mozFullScreenElement ||
1142
- document.msFullscreenElement;
1143
-
1144
- if (isInFullscreen) {
1145
- const exitFullscreen = document.exitFullscreen ||
1146
- document.webkitExitFullscreen ||
1147
- document.mozCancelFullScreen ||
1148
- document.msExitFullscreen;
1149
-
1150
- if (exitFullscreen) {
1151
- exitFullscreen.call(document).then(() => {
1152
- this.isFullscreen = false;
1153
- this.zoomora.classList.remove('fullscreen');
1154
- this.updateButtons();
1155
- }).catch(err => {
1156
- console.warn('Zoomora: Could not exit fullscreen:', err);
1157
- this.fallbackExitFullscreen();
1158
- });
1159
- } else {
1344
+ document.msFullscreenElement
1345
+ );
1346
+
1347
+ if (!isInFullscreen) {
1348
+ // Not in real fullscreen, just exit CSS fullscreen
1349
+ this.fallbackExitFullscreen();
1350
+ return;
1351
+ }
1352
+
1353
+ try {
1354
+ // Try standard API first
1355
+ if (document.exitFullscreen) {
1356
+ document.exitFullscreen()
1357
+ .then(() => {
1358
+ this.isFullscreen = false;
1359
+ this.zoomora.classList.remove('fullscreen');
1360
+ this.updateButtons();
1361
+ })
1362
+ .catch(err => {
1363
+ console.warn('Zoomora: Exit fullscreen failed:', err.message);
1364
+ this.fallbackExitFullscreen();
1365
+ });
1366
+ }
1367
+ // Try webkit
1368
+ else if (document.webkitExitFullscreen) {
1369
+ document.webkitExitFullscreen();
1370
+ this.isFullscreen = false;
1371
+ this.zoomora.classList.remove('fullscreen');
1372
+ this.updateButtons();
1373
+ }
1374
+ // Try moz
1375
+ else if (document.mozCancelFullScreen) {
1376
+ document.mozCancelFullScreen();
1377
+ this.isFullscreen = false;
1378
+ this.zoomora.classList.remove('fullscreen');
1379
+ this.updateButtons();
1380
+ }
1381
+ // Try ms
1382
+ else if (document.msExitFullscreen) {
1383
+ document.msExitFullscreen();
1384
+ this.isFullscreen = false;
1385
+ this.zoomora.classList.remove('fullscreen');
1386
+ this.updateButtons();
1387
+ }
1388
+ // Fallback
1389
+ else {
1160
1390
  this.fallbackExitFullscreen();
1161
1391
  }
1162
- } else {
1392
+ } catch (error) {
1393
+ console.error('Zoomora: Error exiting fullscreen:', error);
1163
1394
  this.fallbackExitFullscreen();
1164
1395
  }
1165
1396
  }
@@ -1215,19 +1446,22 @@ import './styles/zoomora.scss';
1215
1446
  if (!media || !this.isZoomed) return;
1216
1447
 
1217
1448
  this.isDragging = true;
1449
+ media.classList.add('dragging');
1218
1450
  media.style.cursor = 'grabbing';
1219
1451
 
1220
1452
  this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1221
1453
  this.startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1222
1454
 
1223
- // Parse current transform
1224
- const transform = media.style.transform;
1225
- const scaleMatch = transform.match(/scale\(([^)]+)\)/);
1226
- const translateMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
1227
-
1228
- this.currentScale = scaleMatch ? parseFloat(scaleMatch[1]) : 1;
1229
- this.currentX = translateMatch ? parseFloat(translateMatch[1]) : 0;
1230
- this.currentY = translateMatch ? parseFloat(translateMatch[2]) : 0;
1455
+ // Read current position from transform
1456
+ const transform = window.getComputedStyle(media).transform;
1457
+ if (transform && transform !== 'none') {
1458
+ const matrix = new DOMMatrix(transform);
1459
+ this.currentX = matrix.m41;
1460
+ this.currentY = matrix.m42;
1461
+ } else {
1462
+ this.currentX = 0;
1463
+ this.currentY = 0;
1464
+ }
1231
1465
 
1232
1466
  e.preventDefault();
1233
1467
  }
@@ -1238,29 +1472,49 @@ import './styles/zoomora.scss';
1238
1472
  */
1239
1473
  drag(e) {
1240
1474
  if (!this.isDragging) return;
1241
-
1242
1475
  e.preventDefault();
1476
+
1477
+ const media = this.content.querySelector('.zoomora-media');
1478
+ if (!media) return;
1479
+
1243
1480
  const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1244
1481
  const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1245
1482
 
1246
1483
  const deltaX = clientX - this.startX;
1247
1484
  const deltaY = clientY - this.startY;
1248
1485
 
1249
- const newX = this.currentX + deltaX;
1250
- const newY = this.currentY + deltaY;
1251
-
1252
- const media = this.content.querySelector('.zoomora-media');
1253
- if (!media) return;
1486
+ let newX = this.currentX + deltaX;
1487
+ let newY = this.currentY + deltaY;
1254
1488
 
1489
+ // Calculate boundaries for BOTH width AND height
1490
+ const container = this.content.getBoundingClientRect();
1255
1491
  const mediaRect = media.getBoundingClientRect();
1256
- const scale = this.currentScale;
1257
- const maxX = (mediaRect.width * (scale - 1)) / 2;
1258
- const maxY = (mediaRect.height * (scale - 1)) / 2;
1259
1492
 
1260
- const constrainedX = Math.max(-maxX, Math.min(maxX, newX));
1261
- const constrainedY = Math.max(-maxY, Math.min(maxY, newY));
1493
+ // How much the scaled image overflows the container
1494
+ const overflowX = Math.max(0, (mediaRect.width - container.width) / 2);
1495
+ const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
1496
+
1497
+ // Constrain drag to overflow boundaries
1498
+ if (overflowX > 0) {
1499
+ newX = Math.max(-overflowX, Math.min(overflowX, newX));
1500
+ } else {
1501
+ newX = 0; // Center horizontally if no overflow
1502
+ }
1262
1503
 
1263
- media.style.transform = `scale(${scale}) translate(${constrainedX}px, ${constrainedY}px)`;
1504
+ if (overflowY > 0) {
1505
+ newY = Math.max(-overflowY, Math.min(overflowY, newY));
1506
+ } else {
1507
+ newY = 0; // Center vertically if no overflow
1508
+ }
1509
+
1510
+ // Apply transform
1511
+ media.style.transform = `translate(${newX}px, ${newY}px) scale(${this.currentScale})`;
1512
+
1513
+ // Update for next frame
1514
+ this.startX = clientX;
1515
+ this.startY = clientY;
1516
+ this.currentX = newX;
1517
+ this.currentY = newY;
1264
1518
  }
1265
1519
 
1266
1520
  /**
@@ -1270,9 +1524,14 @@ import './styles/zoomora.scss';
1270
1524
  if (!this.isDragging) return;
1271
1525
 
1272
1526
  this.isDragging = false;
1527
+ this.lastDragTime = Date.now();
1528
+
1273
1529
  const media = this.content.querySelector('.zoomora-media');
1274
- if (media && this.isZoomed) {
1275
- media.style.cursor = 'grab';
1530
+ if (media) {
1531
+ media.classList.remove('dragging');
1532
+ if (this.isZoomed) {
1533
+ media.style.cursor = 'grab';
1534
+ }
1276
1535
  }
1277
1536
  }
1278
1537