# Gallery

> A responsive image gallery component with fullscreen viewing, zoom, and gesture support.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/gallery
- Markdown: https://ui.ahmedbna.com/docs/components/gallery.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/gallery.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/gallery.json
- Install: `npx bna-ui add gallery`
- npm dependencies: `expo-haptics`, `expo-image`, `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-safe-area-context`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `useHaptics`, `text`, `view`, `icon`, `spinner`, `button`
- Preview recording: https://demo.ahmedbna.com/0145-gallery-demo.MP4

---

**Example:** A basic image gallery with grid layout and fullscreen viewing

```tsx
// components/demo/gallery/gallery-demo.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import React from 'react';

const sampleImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryDemo() {
  return (
    <Gallery
      items={sampleImages}
      columns={2}
      spacing={8}
      borderRadius={12}
      enableFullscreen={true}
      enableZoom={true}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add gallery
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-image react-native-gesture-handler react-native-reanimated lucide-react-native
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/gallery.tsx
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { useColor } from '@/hooks/useColor';
import { BORDER_RADIUS } from '@/theme/globals';
import { Image } from 'expo-image';
import { Download, Share, X } from 'lucide-react-native';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import {
  FlatList,
  Modal,
  Pressable,
  StyleSheet,
  useWindowDimensions,
  View,
} from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export interface GalleryItem {
  id: string;
  uri: string;
  title?: string;
  description?: string;
  thumbnail?: string;
}

interface GalleryProps {
  items: GalleryItem[];
  columns?: number;
  spacing?: number;
  borderRadius?: number;
  aspectRatio?: number;
  showPages?: boolean;
  showTitles?: boolean;
  showDescriptions?: boolean;
  enableFullscreen?: boolean;
  enableZoom?: boolean;
  enableDownload?: boolean;
  enableShare?: boolean;
  onItemPress?: (item: GalleryItem, index: number) => void;
  onDownload?: (item: GalleryItem) => void;
  onShare?: (item: GalleryItem) => void;
  renderCustomOverlay?: (item: GalleryItem, index: number) => React.ReactNode;
}

const AnimatedImage = Animated.createAnimatedComponent(Image);

// Improved zoom hook with better gesture handling
interface UseImageZoomProps {
  enableZoom: boolean;
  onSetCanSwipe: (canSwipe: boolean) => void;
  shouldReset?: boolean; // Indicates if the current image has changed and zoom should reset
  screenWidth: number;
  screenHeight: number;
}

export const useImageZoom = ({
  enableZoom,
  onSetCanSwipe,
  shouldReset = false,
  screenWidth,
  screenHeight,
}: UseImageZoomProps) => {
  // Shared values for animated properties
  const scale = useSharedValue(1);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  // Saved values to store the state at the start of a gesture
  const savedScale = useSharedValue(1);
  const savedTranslateX = useSharedValue(0);
  const savedTranslateY = useSharedValue(0);

  // Shared value to dynamically enable/disable the pan gesture for dragging
  const panGestureEnabled = useSharedValue(false); // Initially disabled

  // Minimum and maximum zoom scale
  const minScale = 0.8;
  const maxScale = 4;

  // Function to reset the image to its initial state (no zoom, no translation)
  const resetZoom = useCallback(() => {
    'worklet'; // Marks this function to run on the UI thread
    scale.value = withSpring(1, { damping: 20, stiffness: 300 });
    translateX.value = withSpring(0, { damping: 20, stiffness: 300 });
    translateY.value = withSpring(0, { damping: 20, stiffness: 300 });
    savedScale.value = 1;
    savedTranslateX.value = 0;
    savedTranslateY.value = 0;
    // Allow the parent FlatList to swipe when the image is reset
    runOnJS(onSetCanSwipe)(true);
    panGestureEnabled.value = false; // Disable pan gesture when reset
  }, [
    scale,
    translateX,
    translateY,
    savedScale,
    savedTranslateX,
    savedTranslateY,
    onSetCanSwipe,
    panGestureEnabled,
  ]);

  // Effect to reset zoom when the `shouldReset` prop changes (meaning a new image is selected)
  useEffect(() => {
    if (shouldReset) {
      resetZoom();
    }
  }, [shouldReset, resetZoom]);

  // Function to constrain the image translation within its bounds
  const constrainTranslation = useCallback(
    (newScale: number, newTranslateX: number, newTranslateY: number) => {
      'worklet';
      // Calculate maximum allowed translation based on current scale
      const maxTranslateX = Math.max(
        0,
        (screenWidth * newScale - screenWidth) / 2
      );
      const maxTranslateY = Math.max(
        0,
        (screenHeight * newScale - screenHeight) / 2
      );

      // Constrain the new translation values
      const constrainedX = Math.max(
        -maxTranslateX,
        Math.min(maxTranslateX, newTranslateX)
      );
      const constrainedY = Math.max(
        -maxTranslateY,
        Math.min(maxTranslateY, newTranslateY)
      );

      return { x: constrainedX, y: constrainedY };
    },
    [screenWidth, screenHeight]
  );

  // Gesture for double-tapping to zoom in/out
  const doubleTapGesture = Gesture.Tap()
    .numberOfTaps(2)
    .onEnd((event) => {
      if (!enableZoom) return; // Only process if zoom is enabled
      ('worklet');

      // If already zoomed in (beyond a small threshold), reset to original size
      if (scale.value > 1.1) {
        resetZoom(); // This will handle setting panGestureEnabled and canSwipe
      } else {
        // Otherwise, zoom to a target scale (e.g., 2.5x)
        const targetScale = 2.5;
        // Calculate tap position relative to the center of the screen
        const tapX = event.x - screenWidth / 2;
        const tapY = event.y - screenHeight / 2;

        // Calculate new translation to make the tapped point the new center
        const newTranslateX = (-tapX * (targetScale - 1)) / targetScale;
        const newTranslateY = (-tapY * (targetScale - 1)) / targetScale;

        // Constrain translation to keep image within bounds
        const constrained = constrainTranslation(
          targetScale,
          newTranslateX,
          newTranslateY
        );

        // Animate scale and translation
        scale.value = withSpring(targetScale, { damping: 20, stiffness: 300 });
        translateX.value = withSpring(constrained.x, {
          damping: 20,
          stiffness: 300,
        });
        translateY.value = withSpring(constrained.y, {
          damping: 20,
          stiffness: 300,
        });

        // Save current state
        savedScale.value = targetScale;
        savedTranslateX.value = constrained.x;
        savedTranslateY.value = constrained.y;

        // Disable parent FlatList swiping as image is now zoomed
        runOnJS(onSetCanSwipe)(false);
        panGestureEnabled.value = true; // Enable pan gesture for dragging zoomed image
      }
    });

  // Gesture for pinching to zoom
  const pinchGesture = Gesture.Pinch()
    .onStart(() => {
      if (!enableZoom) return;
      ('worklet');
      // Save current state at the start of the pinch
      savedScale.value = scale.value;
      savedTranslateX.value = translateX.value;
      savedTranslateY.value = translateY.value;
    })
    .onUpdate((event) => {
      if (!enableZoom) return;
      ('worklet');

      // Calculate new scale, clamping it between min and max
      const newScale = Math.max(
        minScale,
        Math.min(maxScale, savedScale.value * event.scale)
      );

      // Calculate focal point relative to the image center
      const focalX = event.focalX - screenWidth / 2;
      const focalY = event.focalY - screenHeight / 2;

      // Calculate new translation to keep the focal point in place during zoom
      const scaleDiff = newScale / savedScale.value;
      const newTranslateX = savedTranslateX.value + focalX * (1 - scaleDiff);
      const newTranslateY = savedTranslateY.value + focalY * (1 - scaleDiff);

      // Constrain translation
      const constrained = constrainTranslation(
        newScale,
        newTranslateX,
        newTranslateY
      );

      // Apply new scale and translation
      scale.value = newScale;
      translateX.value = constrained.x;
      translateY.value = constrained.y;

      // Dynamically enable/disable panGestureEnabled and FlatList scrolling based on zoom level
      panGestureEnabled.value = newScale > 1.1;
      runOnJS(onSetCanSwipe)(newScale <= 1.1); // FlatList scrollable if not zoomed
    })
    .onEnd(() => {
      if (!enableZoom) return;
      ('worklet');

      // If zoomed out too much, reset to original size
      if (scale.value < 1) {
        resetZoom(); // This will handle setting panGestureEnabled and canSwipe
      } else {
        // Save current state after pinch ends
        savedScale.value = scale.value;
        savedTranslateX.value = translateX.value;
        savedTranslateY.value = translateY.value;
        // Re-evaluate if panGestureEnabled and FlatList should be able to swipe
        panGestureEnabled.value = scale.value > 1.1;
        runOnJS(onSetCanSwipe)(scale.value <= 1.1);
      }
    });

  // Gesture for panning (dragging) the image when zoomed in
  const panGesture = Gesture.Pan()
    .minPointers(1) // This gesture will respond to a single finger
    .maxPointers(1)
    .enabled(panGestureEnabled.value) // Only enabled if panGestureEnabled.value is true
    .onStart(() => {
      'worklet';
      // If this onStart is called, it means the gesture is enabled and recognized.
      savedTranslateX.value = translateX.value;
      savedTranslateY.value = translateY.value;
      runOnJS(onSetCanSwipe)(false); // Disable parent FlatList swipe when dragging zoomed image
    })
    .onUpdate((event) => {
      'worklet';
      // This check is a safeguard, but 'enabled' should prevent this from being called if not zoomed.
      if (!enableZoom || !panGestureEnabled.value) return;

      // Calculate new translation based on drag
      const newTranslateX = savedTranslateX.value + event.translationX;
      const newTranslateY = savedTranslateY.value + event.translationY;

      // Constrain translation
      const constrained = constrainTranslation(
        scale.value,
        newTranslateX,
        newTranslateY
      );

      // Apply new translation
      translateX.value = constrained.x;
      translateY.value = constrained.y;
    })
    .onEnd(() => {
      'worklet';
      savedTranslateX.value = translateX.value;
      savedTranslateY.value = translateY.value;
      // Re-enable FlatList swipe if not zoomed after pan ends
      runOnJS(onSetCanSwipe)(scale.value <= 1.1);
    });

  // Compose all gestures:
  // - Race: Double tap takes precedence if detected.
  // - Simultaneous: Pinch and dynamically enabled single-finger pan can happen at the same time.
  const composedGesture = Gesture.Race(
    doubleTapGesture,
    Gesture.Simultaneous(pinchGesture, panGesture)
  );

  // Animated style for the image based on scale and translation values
  const animatedImageStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { scale: scale.value },
        { translateX: translateX.value },
        { translateY: translateY.value },
      ],
    };
  });

  return {
    animatedImageStyle,
    composedGesture,
    resetZoom,
  };
};

// Fixed fullscreen image component
interface FullscreenImageProps {
  item: GalleryItem;
  index: number;
  selectedIndex: number;
  enableZoom: boolean;
  // Callback to inform the parent FlatList whether it should be scrollable
  onSetCanSwipe: (canSwipe: boolean) => void;
  screenWidth: number;
  screenHeight: number;
}

const FullscreenImage = memo(
  ({
    item,
    index,
    selectedIndex,
    enableZoom,
    onSetCanSwipe,
    screenWidth,
    screenHeight,
  }: FullscreenImageProps) => {
    // Determine if this image is the currently selected one to trigger zoom reset
    const shouldReset = index === selectedIndex;
    const backgroundColor = useColor('background');
    const { animatedImageStyle, composedGesture } = useImageZoom({
      enableZoom,
      onSetCanSwipe, // Pass the callback to the hook
      shouldReset,
      screenWidth,
      screenHeight,
    });

    return (
      <View
        style={{
          width: screenWidth,
          height: screenHeight,
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor,
        }}
      >
        {/* GestureDetector always present if zoom is enabled */}
        {enableZoom ? (
          <GestureDetector gesture={composedGesture}>
            <Animated.View
              style={[
                styles.imageContainer,
                { width: screenWidth, height: screenHeight },
              ]}
            >
              <AnimatedImage
                source={{ uri: item.uri }}
                style={[
                  { width: screenWidth, height: screenHeight },
                  animatedImageStyle,
                ]}
                contentFit='contain'
              />
            </Animated.View>
          </GestureDetector>
        ) : (
          // If zoom is not enabled, render without GestureDetector
          <Animated.View
            style={[
              styles.imageContainer,
              { width: screenWidth, height: screenHeight },
            ]}
          >
            <AnimatedImage
              source={{ uri: item.uri }}
              style={[
                { width: screenWidth, height: screenHeight },
                animatedImageStyle,
              ]}
              contentFit='contain'
            />
          </Animated.View>
        )}
      </View>
    );
  }
);

export function Gallery({
  items,
  columns = 4,
  spacing = 0,
  aspectRatio = 1,
  borderRadius = 0,
  showPages = false,
  showTitles = false,
  showDescriptions = false,
  enableFullscreen = true,
  enableZoom = true,
  enableDownload = false,
  enableShare = false,
  onItemPress,
  onDownload,
  onShare,
  renderCustomOverlay,
}: GalleryProps) {
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
  const insets = useSafeAreaInsets();
  // State for the currently selected image index in fullscreen mode
  const [selectedIndex, setSelectedIndex] = useState<number>(-1);
  // State to control modal visibility
  const [isModalVisible, setIsModalVisible] = useState(false);
  // State for the calculated width of the gallery container
  const [containerWidth, setContainerWidth] = useState(screenWidth);
  // State to control whether the fullscreen FlatList can be swiped horizontally
  const [flatListScrollEnabled, setFlatListScrollEnabled] = useState(true);

  // Refs for the FlatList components
  const fullscreenFlatListRef = useRef<FlatList>(null);
  const thumbnailFlatListRef = useRef<FlatList>(null);

  // Theme colors using custom hook
  const textColor = useColor('text');
  const primary = useColor('primary');
  const mutedColor = useColor('textMuted');
  const backgroundColor = useColor('background');
  const secondary = useColor('secondary');

  // Calculate item width for the grid based on container width, columns, and spacing
  const itemWidth = (containerWidth - spacing * (columns - 1)) / columns;

  // Function to open the fullscreen modal
  const openFullscreen = useCallback(
    (index: number) => {
      if (!enableFullscreen) return; // Only open if fullscreen is enabled
      setSelectedIndex(index);
      setIsModalVisible(true);
      // Initially, allow FlatList scrolling
      setFlatListScrollEnabled(true);

      // Use setTimeout to ensure the modal is fully rendered before trying to scroll the FlatList
      setTimeout(() => {
        fullscreenFlatListRef.current?.scrollToIndex({
          index,
          animated: false,
        });
        thumbnailFlatListRef.current?.scrollToIndex({
          index,
          animated: false,
          viewPosition: 0.5, // Center the thumbnail
        });
      }, 100);
    },
    [enableFullscreen]
  );

  // Function to close the fullscreen modal
  const closeFullscreen = useCallback(() => {
    setIsModalVisible(false);
    setSelectedIndex(-1); // Reset selected index
    setFlatListScrollEnabled(true); // Ensure scrolling is re-enabled on close
  }, []);

  // Handler for pressing a gallery item (thumbnail)
  const handleItemPress = useCallback(
    (item: GalleryItem, index: number) => {
      if (onItemPress) {
        onItemPress(item, index); // Call custom press handler if provided
      } else if (enableFullscreen) {
        openFullscreen(index); // Otherwise, open fullscreen
      }
    },
    [onItemPress, enableFullscreen, openFullscreen]
  );

  // Handler for pressing a thumbnail in the fullscreen bottom bar
  const handleThumbnailPress = useCallback((index: number) => {
    setSelectedIndex(index); // Update selected index
    setFlatListScrollEnabled(true); // Always allow swiping when a thumbnail is tapped
    fullscreenFlatListRef.current?.scrollToIndex({
      index,
      animated: true,
    });
  }, []);

  // Callback for FlatList to detect when viewable items change (for updating selected index)
  const onViewableItemsChanged = useCallback(
    ({ viewableItems }: any) => {
      if (viewableItems.length > 0) {
        const newIndex = viewableItems[0].index;
        if (
          newIndex !== selectedIndex &&
          newIndex !== null &&
          newIndex !== undefined
        ) {
          setSelectedIndex(newIndex);
          // Sync thumbnail scroll to the newly selected image
          setTimeout(() => {
            thumbnailFlatListRef.current?.scrollToIndex({
              index: newIndex,
              animated: true,
              viewPosition: 0.5, // Center the thumbnail
            });
          }, 100);
        }
      }
    },
    [selectedIndex]
  );

  // Configuration for viewability of FlatList items
  const viewabilityConfig = {
    itemVisiblePercentThreshold: 50, // An item is "viewable" if 50% of it is visible
  };

  // Helper to get the currently displayed item in fullscreen
  const getCurrentItem = useCallback(() => {
    return selectedIndex >= 0 && selectedIndex < items.length
      ? items[selectedIndex]
      : null;
  }, [selectedIndex, items]);

  // Handler for download button
  const handleDownload = useCallback(() => {
    const currentItem = getCurrentItem();
    if (currentItem && onDownload) {
      onDownload(currentItem);
    }
  }, [getCurrentItem, onDownload]);

  // Handler for share button
  const handleShare = useCallback(() => {
    const currentItem = getCurrentItem();
    if (currentItem && onShare) {
      onShare(currentItem);
    }
  }, [getCurrentItem, onShare]);

  // Render function for each item in the grid gallery
  const renderGalleryItem = useCallback(
    ({ item, index }: { item: GalleryItem; index: number }) => (
      <Pressable
        key={item.id}
        style={[
          {
            width: itemWidth,
            height: itemWidth * aspectRatio,
            borderRadius,
          },
        ]}
        onPress={() => handleItemPress(item, index)}
      >
        <Image
          source={{ uri: item.thumbnail || item.uri }} // Use thumbnail if available, otherwise full URI
          style={[styles.gridImage, { borderRadius }]}
          contentFit='cover'
          transition={200}
        />

        {/* Render custom overlay if provided */}
        {renderCustomOverlay && renderCustomOverlay(item, index)}

        {/* Display title and description if enabled */}
        {(showTitles || showDescriptions) && (
          <View style={[styles.itemInfo]}>
            {showTitles && item.title && (
              <Text
                variant='subtitle'
                numberOfLines={1}
                style={{ color: textColor }}
              >
                {item.title}
              </Text>
            )}
            {showDescriptions && item.description && (
              <Text
                variant='caption'
                numberOfLines={2}
                style={{ color: mutedColor }}
              >
                {item.description}
              </Text>
            )}
          </View>
        )}
      </Pressable>
    ),
    [
      itemWidth,
      aspectRatio,
      borderRadius,
      handleItemPress,
      renderCustomOverlay,
      showTitles,
      showDescriptions,
      textColor,
      mutedColor,
    ]
  );

  // Render function for each item in the fullscreen FlatList
  const renderFullscreenItem = useCallback(
    ({ item, index }: { item: GalleryItem; index: number }) => (
      <FullscreenImage
        key={`fullscreen-${item.id}`} // Unique key for fullscreen items
        item={item}
        index={index}
        selectedIndex={selectedIndex} // Pass selected index for zoom reset logic
        enableZoom={enableZoom}
        onSetCanSwipe={setFlatListScrollEnabled} // Pass the callback to control parent FlatList's scroll
        screenWidth={screenWidth}
        screenHeight={screenHeight}
      />
    ),
    [enableZoom, selectedIndex, screenWidth, screenHeight]
  );

  // Render controls for the fullscreen modal (top and bottom bars)
  const renderFullscreenControls = () => {
    const currentItem = getCurrentItem();

    return (
      <View style={styles.fullscreenControls} pointerEvents='box-none'>
        {/* Top controls (share, download, close) */}
        <View
          style={[
            styles.topControls,
            { backgroundColor, paddingTop: insets.top + 12 },
          ]}
        >
          <View style={styles.topRightControls}>
            {enableDownload && onDownload && (
              <Button size='icon' variant='ghost' onPress={handleDownload}>
                <Download size={24} color={primary} />
              </Button>
            )}
            {enableShare && onShare && (
              <Button size='icon' variant='ghost' onPress={handleShare}>
                <Share size={24} color={primary} />
              </Button>
            )}
          </View>

          <Button size='icon' variant='ghost' onPress={closeFullscreen}>
            <X size={26} color={primary} />
          </Button>
        </View>

        {/* Bottom controls (page, title, description, thumbnails) */}
        <View
          style={[
            styles.bottomControls,
            { backgroundColor, paddingBottom: insets.bottom + 16 },
          ]}
        >
          {showPages && (
            <Text
              variant='caption'
              style={{
                textAlign: 'center',
                marginBottom: 8,
                color: mutedColor,
              }}
            >
              {selectedIndex + 1} of {items.length}
            </Text>
          )}

          {currentItem?.title && (
            <Text
              variant='subtitle'
              style={{ textAlign: 'center', marginBottom: 8, color: textColor }}
              numberOfLines={1}
            >
              {currentItem.title}
            </Text>
          )}

          {currentItem?.description && (
            <Text
              variant='caption'
              style={{
                textAlign: 'center',
                marginBottom: 16,
                color: mutedColor,
              }}
              numberOfLines={2}
            >
              {currentItem.description}
            </Text>
          )}

          {/* Horizontal FlatList for thumbnails */}
          <FlatList
            ref={thumbnailFlatListRef}
            data={items}
            renderItem={({ item, index }) => (
              <Pressable
                style={[
                  styles.thumbnailItem,
                  selectedIndex === index && {
                    borderColor: secondary, // Highlight selected thumbnail
                    borderWidth: 2,
                  },
                ]}
                onPress={() => handleThumbnailPress(index)}
              >
                <Image
                  source={{ uri: item.thumbnail || item.uri }}
                  style={styles.thumbnailImage}
                  contentFit='cover'
                />
              </Pressable>
            )}
            keyExtractor={(item) => item.id}
            horizontal
            showsHorizontalScrollIndicator={false}
            contentContainerStyle={styles.thumbnailContainer}
            ItemSeparatorComponent={() => <View style={{ width: 8 }} />} // Spacing between thumbnails
            getItemLayout={(data, index) => ({
              length: 48, // Fixed item length for layout calculation
              offset: 56 * index, // Offset for each item (item length + separator width)
              index,
            })}
          />
        </View>
      </View>
    );
  };

  // Render empty state if no items are provided
  if (items.length === 0) {
    return (
      <View style={[styles.emptyState]}>
        <Text variant='subtitle' style={{ color: mutedColor }}>
          No images to display
        </Text>
      </View>
    );
  }

  return (
    // GestureHandlerRootView is required for React Native Gesture Handler to work
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* FlatList for the main gallery grid — the previous plain ScrollView
          + .map() rendered every item eagerly regardless of scroll
          position, backwards from the less-visible fullscreen viewer below,
          which already virtualizes via FlatList. */}
      <FlatList
        key={`gallery-${columns}`}
        data={items}
        numColumns={columns}
        renderItem={renderGalleryItem}
        keyExtractor={(item) => item.id}
        style={[styles.container, { backgroundColor }]}
        contentContainerStyle={{ gap: spacing }}
        columnWrapperStyle={columns > 1 ? { gap: spacing } : undefined}
        showsVerticalScrollIndicator={false}
        // Measure the container width on layout to calculate item widths dynamically
        onLayout={(event) => {
          const { width } = event.nativeEvent.layout;
          setContainerWidth(width);
        }}
      />

      {/* Modal for fullscreen image view */}
      <Modal
        visible={isModalVisible}
        transparent
        animationType='fade'
        onRequestClose={closeFullscreen}
      >
        <View style={{ flex: 1, backgroundColor }}>
          {/* GestureHandlerRootView for gestures within the modal */}
          <GestureHandlerRootView style={{ flex: 1 }}>
            {/* FlatList for horizontal swiping of fullscreen images */}
            <FlatList
              ref={fullscreenFlatListRef}
              data={items}
              renderItem={renderFullscreenItem}
              keyExtractor={(item) => item.id}
              horizontal
              pagingEnabled // Enables snap-to-page behavior for horizontal swiping
              showsHorizontalScrollIndicator={false}
              onViewableItemsChanged={onViewableItemsChanged} // Detect when current image changes
              viewabilityConfig={viewabilityConfig}
              getItemLayout={(data, index) => ({
                length: screenWidth, // Each item takes full screen width
                offset: screenWidth * index,
                index,
              })}
              scrollEnabled={flatListScrollEnabled} // Control FlatList scrolling based on zoom state
              removeClippedSubviews={false} // Important for images that are partially off-screen due to zoom
              initialNumToRender={3}
              maxToRenderPerBatch={3}
              windowSize={21}
            />
          </GestureHandlerRootView>
          {/* Render fullscreen controls overlay */}
          {renderFullscreenControls()}
        </View>
      </Modal>
    </GestureHandlerRootView>
  );
}

// Stylesheet for the component
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  gridImage: {
    flex: 1,
  },
  itemInfo: {
    padding: 8,
  },
  emptyState: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 32,
    borderRadius: BORDER_RADIUS,
    margin: 16,
  },
  imageContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  fullscreenControls: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    // Ensure controls don't block interaction with the image itself unless explicitly on a button
    pointerEvents: 'box-none',
  },
  topControls: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingBottom: 16,
  },
  topRightControls: {
    gap: 8,
    flexDirection: 'row',
  },
  bottomControls: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    padding: 16,
  },
  thumbnailContainer: {
    paddingHorizontal: 16,
    alignItems: 'center', // Vertically center thumbnails
  },
  thumbnailItem: {
    width: 40,
    height: 40,
    borderRadius: 8,
    borderWidth: 1,
    overflow: 'hidden',
    borderColor: 'transparent',
  },
  thumbnailImage: {
    width: '100%',
    height: '100%',
  },
});
```

**3.** Update the import paths to match your project setup.

**4.** Make sure to configure react-native-gesture-handler and
react-native-reanimated in your app according to their installation guides.

## Usage

```tsx
import { Gallery } from '@/components/ui/gallery';
```

```tsx
const galleryItems = [
  {
    id: '1',
    uri: 'https://picsum.photos/400/400?random=1',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
  },
  {
    id: '2',
    uri: 'https://picsum.photos/400/400?random=2',
    title: 'City Skyline',
    description: 'Urban architecture at its finest',
  },
  // ... more items
];

<Gallery items={galleryItems} />;
```

## Examples

#### Default

**Example:** A basic image gallery with grid layout and fullscreen viewing

```tsx
// components/demo/gallery/gallery-demo.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import React from 'react';

const sampleImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryDemo() {
  return (
    <Gallery
      items={sampleImages}
      columns={2}
      spacing={8}
      borderRadius={12}
      enableFullscreen={true}
      enableZoom={true}
    />
  );
}
```

#### Custom Grid

**Example:** Gallery with custom columns, spacing, and aspect ratio

```tsx
// components/demo/gallery/gallery-grid.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import { ScrollView } from '@/components/ui/scroll-view';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

const gridImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryGrid() {
  return (
    <ScrollView
      style={{ flex: 1 }}
      contentContainerStyle={{
        gap: 16,
        paddingBottom: 32,
      }}
      showsVerticalScrollIndicator={false}
    >
      <View style={{ flex: 1 }}>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          4 Columns, No Spacing
        </Text>
        <Gallery
          items={gridImages}
          columns={4}
          spacing={0}
          borderRadius={0}
          aspectRatio={1}
        />
      </View>

      <View style={{ flex: 1 }}>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          3 Columns with Spacing
        </Text>

        <Gallery
          items={gridImages.slice(0, 6)}
          columns={3}
          spacing={12}
          borderRadius={8}
          aspectRatio={1.2}
        />
      </View>

      <View style={{ flex: 1 }}>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          2 Columns, Large Spacing
        </Text>
        <Gallery
          items={gridImages.slice(0, 4)}
          columns={2}
          spacing={20}
          borderRadius={16}
          aspectRatio={0.8}
        />
      </View>
    </ScrollView>
  );
}
```

#### With Titles and Descriptions

**Example:** Gallery displaying image titles and descriptions

```tsx
// components/demo/gallery/gallery-info.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import React from 'react';

const infoImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryInfo() {
  return (
    <Gallery
      items={infoImages}
      columns={2}
      spacing={16}
      borderRadius={12}
      aspectRatio={1}
      showTitles={true}
      showDescriptions={true}
      showPages={true}
      enableFullscreen={true}
      enableZoom={true}
    />
  );
}
```

#### Different Layouts

**Example:** Various gallery layouts and configurations

```tsx
// components/demo/gallery/gallery-layouts.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import { ScrollView } from '@/components/ui/scroll-view';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

const layoutImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryLayouts() {
  return (
    <ScrollView
      style={{ flex: 1 }}
      contentContainerStyle={{
        gap: 16,
        paddingBottom: 32,
      }}
      showsVerticalScrollIndicator={false}
    >
      <View>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          Single Column (Feed Style)
        </Text>
        <Gallery
          items={layoutImages.slice(0, 3)}
          columns={1}
          spacing={16}
          borderRadius={12}
          aspectRatio={1.2}
          showTitles={true}
        />
      </View>

      <View>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          Square Grid
        </Text>
        <Gallery
          items={layoutImages}
          columns={3}
          spacing={4}
          borderRadius={8}
          aspectRatio={1}
        />
      </View>

      <View>
        <Text variant='subtitle' style={{ marginBottom: 12 }}>
          Wide Thumbnails
        </Text>
        <Gallery
          items={layoutImages.slice(0, 4)}
          columns={2}
          spacing={8}
          borderRadius={16}
          aspectRatio={1.5}
        />
      </View>
    </ScrollView>
  );
}
```

#### With Controls

**Example:** Images with controls

```tsx
// components/demo/gallery/gallery-controls.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import React from 'react';
import { Alert } from 'react-native';

const controlImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryControls() {
  const handleDownload = (item: GalleryItem) => {
    Alert.alert('Download', `Downloading: ${item.title || 'Image'}`, [
      { text: 'OK' },
    ]);
  };

  const handleShare = (item: GalleryItem) => {
    Alert.alert('Share', `Sharing: ${item.title || 'Image'}`, [{ text: 'OK' }]);
  };

  return (
    <Gallery
      items={controlImages}
      columns={2}
      spacing={12}
      borderRadius={12}
      aspectRatio={1}
      showTitles={true}
      showDescriptions={true}
      showPages={true}
      enableFullscreen={true}
      enableZoom={true}
      enableDownload={true}
      enableShare={true}
      onDownload={handleDownload}
      onShare={handleShare}
    />
  );
}
```

#### With Overlays

**Example:** Images with overlay

```tsx
// components/demo/gallery/gallery-overlay.tsx
import { Gallery, GalleryItem } from '@/components/ui/gallery';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

const sampleImages: GalleryItem[] = [
  {
    id: '1',
    uri: 'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'City Skyline',
    description: 'Modern architecture at sunset',
    thumbnail:
      'https://images.unsplash.com/photo-1637858868799-7f26a0640eb6?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '2',
    uri: 'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Winter Wonderland',
    description: 'Snow-covered peaks and pristine wilderness',
    thumbnail:
      'https://images.unsplash.com/photo-1644190022446-04b99df7259a?q=80&w=2012&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '3',
    uri: 'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Ocean Waves',
    description: 'Peaceful ocean scene with rolling waves',
    thumbnail:
      'https://images.unsplash.com/photo-1717732596477-04f8c5d53387?q=80&w=987&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '4',
    uri: 'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Forest Path',
    description: 'A winding path through ancient trees',
    thumbnail:
      'https://images.unsplash.com/photo-1575737698350-52e966f924d4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '5',
    uri: 'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Desert Dunes',
    description: 'Golden sand dunes stretching to the horizon',
    thumbnail:
      'https://images.unsplash.com/photo-1667830867718-da7f5a45d20d?q=80&w=1064&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    id: '6',
    uri: 'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    title: 'Beautiful Landscape',
    description: 'A stunning view of mountains and valleys',
    thumbnail:
      'https://images.unsplash.com/photo-1593085512500-5d55148d6f0d?q=80&w=2334&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
];

export function GalleryOverlay() {
  return (
    <Gallery
      items={sampleImages}
      columns={2}
      aspectRatio={1}
      renderCustomOverlay={(item) => (
        <View
          style={{
            position: 'absolute',
            top: 8,
            right: 8,
            backgroundColor: 'rgba(0,0,0,0.7)',
            borderRadius: 12,
            paddingHorizontal: 8,
            paddingVertical: 4,
          }}
        >
          <Text
            variant='caption'
            style={{
              color: 'white',
              fontSize: 12,
              fontWeight: 'bold',
            }}
          >
            {item.title}
          </Text>
        </View>
      )}
    />
  );
}
```

## API Reference

### Gallery

The main gallery component that displays images in a grid layout with fullscreen viewing capabilities.

| Prop                  | Type                                                    | Default | Description                                    |
| --------------------- | ------------------------------------------------------- | ------- | ---------------------------------------------- |
| `items`               | `GalleryItem[]`                                         | -       | Array of gallery items to display.             |
| `columns`             | `number`                                                | `4`     | Number of columns in the grid layout.          |
| `spacing`             | `number`                                                | `0`     | Spacing between grid items in pixels.          |
| `borderRadius`        | `number`                                                | `0`     | Border radius for gallery items.               |
| `aspectRatio`         | `number`                                                | `1`     | Aspect ratio for gallery items (width/height). |
| `showPages`           | `boolean`                                               | `false` | Show page indicator in fullscreen mode.        |
| `showTitles`          | `boolean`                                               | `false` | Show image titles in grid view.                |
| `showDescriptions`    | `boolean`                                               | `false` | Show image descriptions in grid view.          |
| `enableFullscreen`    | `boolean`                                               | `true`  | Enable fullscreen viewing mode.                |
| `enableZoom`          | `boolean`                                               | `true`  | Enable zoom functionality in fullscreen mode.  |
| `enableDownload`      | `boolean`                                               | `false` | Show download button in fullscreen mode.       |
| `enableShare`         | `boolean`                                               | `false` | Show share button in fullscreen mode.          |
| `onItemPress`         | `(item: GalleryItem, index: number) => void`            | -       | Custom handler for item press events.          |
| `onDownload`          | `(item: GalleryItem) => void`                           | -       | Handler for download button press.             |
| `onShare`             | `(item: GalleryItem) => void`                           | -       | Handler for share button press.                |
| `renderCustomOverlay` | `(item: GalleryItem, index: number) => React.ReactNode` | -       | Custom overlay renderer for grid items.        |

### GalleryItem

Interface for gallery items.

| Prop          | Type     | Required | Description                                   |
| ------------- | -------- | -------- | --------------------------------------------- |
| `id`          | `string` | Yes      | Unique identifier for the gallery item.       |
| `uri`         | `string` | Yes      | Image URI for the full-size image.            |
| `title`       | `string` | No       | Optional title for the image.                 |
| `description` | `string` | No       | Optional description for the image.           |
| `thumbnail`   | `string` | No       | Optional thumbnail URI (falls back to `uri`). |

## Features

### Gesture Support

- **Pinch to zoom**: Zoom in and out on images in fullscreen mode
- **Double tap to zoom**: Quick zoom to 2.5x scale or reset to original size
- **Pan when zoomed**: Drag zoomed images to view different areas
- **Swipe navigation**: Horizontal swipe to navigate between images

### Fullscreen Mode

- Immersive fullscreen viewing experience
- Thumbnail navigation bar at the bottom
- Top controls for sharing, downloading, and closing
- Page indicators showing current position

### Responsive Design

- Automatically adjusts to screen size
- Configurable grid columns and spacing
- Maintains aspect ratios across different devices
- Smooth animations and transitions

### Performance Optimizations

- Lazy loading of off-screen images
- Efficient gesture handling with React Native Reanimated
- Optimized FlatList rendering for large galleries
- Thumbnail support for faster grid loading

## Accessibility

The Gallery component includes accessibility features:

- Proper keyboard navigation support
- Screen reader compatibility
- High contrast support for UI elements
- Semantic structure for assistive technologies
- Touch target sizes meet accessibility guidelines

## Troubleshooting

### Common Issues

**Gestures not working:**

- Ensure `react-native-gesture-handler` is properly installed and configured
- Make sure the component is wrapped in `GestureHandlerRootView`

**Images not loading:**

- Verify image URIs are accessible
- Check network connectivity
- Ensure proper CORS configuration for web images

**Performance issues with large galleries:**

- Consider implementing pagination
- Use thumbnail images for grid view
- Optimize image sizes and formats

**Zoom not working properly:**

- Verify `react-native-reanimated` is correctly set up
- Check that `enableZoom` prop is set to `true`
- Ensure proper gesture handler configuration
