diff --git a/src/components/products/MediaGallery.tsx b/src/components/products/MediaGallery.tsx
index dbe2868b..ffc80ed4 100644
--- a/src/components/products/MediaGallery.tsx
+++ b/src/components/products/MediaGallery.tsx
@@ -4,9 +4,12 @@ import type { Media } from "@spree/sdk";
import { ZoomIn } from "lucide-react";
import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
-import { useState } from "react";
+import { useCallback, useRef, useState } from "react";
import { ProductImage } from "@/components/ui/product-image";
+const SWIPE_THRESHOLD_PX = 50;
+const SWIPE_MAX_VERTICAL_PX = 75;
+
/** Tiny 10×10 neutral gray PNG used as a blur placeholder while images load. */
const BLUR_PLACEHOLDER =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIElEQVQYV2P4////MwwMDAxMDAwMDGQJMJCvkGwNZCsEAGebBwVss9lRAAAAAElFTkSuQmCC";
@@ -63,6 +66,47 @@ function MediaGalleryInner({
null,
);
+ const safeIndex = Math.max(0, Math.min(selectedIndex, images.length - 1));
+
+ // Horizontal swipe on the main image navigates between media. When a
+ // swipe is detected we suppress the synthetic click so the lightbox
+ // doesn't open from the same gesture.
+ const touchStartRef = useRef<{ x: number; y: number } | null>(null);
+ const suppressClickRef = useRef(false);
+
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
+ const touch = e.touches[0];
+ if (!touch) return;
+ touchStartRef.current = { x: touch.clientX, y: touch.clientY };
+ suppressClickRef.current = false;
+ }, []);
+
+ const handleTouchEnd = useCallback(
+ (e: React.TouchEvent) => {
+ const start = touchStartRef.current;
+ touchStartRef.current = null;
+ if (!start || images.length <= 1) return;
+ const touch = e.changedTouches[0];
+ if (!touch) return;
+ const dx = touch.clientX - start.x;
+ const dy = touch.clientY - start.y;
+ if (
+ Math.abs(dx) < SWIPE_THRESHOLD_PX ||
+ Math.abs(dy) > SWIPE_MAX_VERTICAL_PX
+ ) {
+ return;
+ }
+ suppressClickRef.current = true;
+ const nextIndex =
+ dx < 0
+ ? (safeIndex + 1) % images.length
+ : (safeIndex - 1 + images.length) % images.length;
+ setSelectedIndex(nextIndex);
+ setMainImageErrorUrl(null);
+ },
+ [images.length, safeIndex],
+ );
+
if (images.length === 0) {
return (
@@ -81,7 +125,6 @@ function MediaGalleryInner({
setMainImageErrorUrl(null);
};
- const safeIndex = Math.max(0, Math.min(selectedIndex, images.length - 1));
const selectedImage = images[safeIndex];
const mainImageUrl = getMainImageUrl(selectedImage);
const showMainImage = mainImageUrl && mainImageErrorUrl !== mainImageUrl;
@@ -91,8 +134,16 @@ function MediaGalleryInner({
{/* Main Image */}