Products lookbook

import React from 'react';
import { View, Text, Image, Actionable, Flexbox, Icon } from '@evlop/native-components';
import { useProduct, PriceDisplay } from '@evlop/shopify';
import { Animated, Easing, StyleSheet, Pressable } from 'react-native';

interface ProductImage {
  id: string;
  originalSrc: string;
  altText: string;
}

interface Product {
  id: string;
  title: string;
  handle: string;
  images: ProductImage[];
  price?: string;
}

interface Annotation {
  id: string;
  x: number;
  y: number;
  width: number;
  height: number;
  product: Product;
}

interface ImageData {
  url: string;
  aspectRatio: number;
  annotations: Annotation[];
}

interface AppBlockProps {
  data: {
    images: ImageData[];
  };
}

const HOTSPOT_SIZE = 20;
const IMAGE_SIZE = 60;
const RING_COUNT = 3;
const ACTIVE_COLOR = '#4CD964';
const NAV_BUTTON_SIZE = 44;
const IMAGE_TRANSITION_MS = 400;

// ---------------------------------------------------------
// Animated pulse ring
// ---------------------------------------------------------

interface PulseRingProps {
  delay: number;
  isActive: boolean;
}

const PulseRing: React.FC<PulseRingProps> = ({ delay, isActive }) => {
  const progress = React.useRef(new Animated.Value(0)).current;
  const loopRef = React.useRef<Animated.CompositeAnimation | null>(null);
  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);

  React.useEffect(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }

    if (loopRef.current) {
      loopRef.current.stop();
      loopRef.current = null;
    }

    if (isActive) {
      timerRef.current = setTimeout(() => {
        progress.setValue(0);

        loopRef.current = Animated.loop(
          Animated.sequence([
            Animated.timing(progress, {
              toValue: 1,
              duration: 2000,
              easing: Easing.out(Easing.quad),
              useNativeDriver: true,
            }),
            Animated.timing(progress, {
              toValue: 0,
              duration: 2000,
              easing: Easing.out(Easing.quad),
              useNativeDriver: true,
            }),
          ]),
        );

        loopRef.current.start();
      }, delay);
    } else {
      Animated.timing(progress, {
        toValue: 0,
        duration: 300,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true,
      }).start();
    }

    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
      if (loopRef.current) {
        loopRef.current.stop();
        loopRef.current = null;
      }
    };
  }, [delay, isActive, progress]);

  const scale = progress.interpolate({
    inputRange: [0, 1],
    outputRange: [1, 3.5],
  });

  const opacity = progress.interpolate({
    inputRange: [0, 0.3, 0.7, 1],
    outputRange: [0.7, 0.5, 0.2, 0],
  });

  return (
    <Animated.View
      style={[
        styles.pulseRing,
        {
          opacity,
          transform: [{ scale }],
        },
      ]}
    />
  );
};

// ---------------------------------------------------------
// Hotspot + product card
// ---------------------------------------------------------

interface HotspotWithCardProps {
  annotation: Annotation;
  index: number;
  imageWidth: number;
  imageHeight: number;
  isActive: boolean;
  onHotspotPress: (index: number) => void;
}

const HotspotWithCard: React.FC<HotspotWithCardProps> = ({
  annotation,
  index,
  imageWidth,
  imageHeight,
  isActive,
  onHotspotPress,
}) => {
  const [showProduct, setShowProduct] = React.useState(false);

  const product = useProduct(
    annotation.product.handle
      ? { productHandle: annotation.product.handle }
      : { productId: annotation.product.id },
  );

  const contentX = (annotation.x + annotation.width / 2) * imageWidth;
  const contentY = (annotation.y + annotation.height / 2) * imageHeight;

  const annotationWidth = annotation.width * imageWidth;

  const leftSpace = contentX;
  const rightSpace = imageWidth - contentX;
  const isLeft = leftSpace > rightSpace;

  const gap = 8;
  const halfAnnotationWidth = annotationWidth / 2;

  const productX = isLeft
    ? -(halfAnnotationWidth + gap + IMAGE_SIZE)
    : halfAnnotationWidth + gap;

  const productY = -IMAGE_SIZE / 2;
  const lineWidth = halfAnnotationWidth + gap;

  // Idle pulse
  const idlePulse = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    const animation = Animated.loop(
      Animated.sequence([
        Animated.timing(idlePulse, {
          toValue: 1,
          duration: 1500,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: true,
        }),
        Animated.timing(idlePulse, {
          toValue: 0,
          duration: 1500,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: true,
        }),
      ]),
    );

    animation.start();

    return () => animation.stop();
  }, [idlePulse]);

  // Active state
  const activeScale = React.useRef(new Animated.Value(1)).current;
  const activeGlow = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    Animated.parallel([
      Animated.timing(activeScale, {
        toValue: isActive ? 1.2 : 1,
        duration: isActive ? 300 : 250,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true,
      }),
      Animated.timing(activeGlow, {
        toValue: isActive ? 1 : 0,
        duration: isActive ? 400 : 300,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true,
      }),
    ]).start();
  }, [activeGlow, activeScale, isActive]);

  const lineProgress = React.useRef(new Animated.Value(0)).current;
  const productProgress = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    let lineTimer: ReturnType<typeof setTimeout> | null = null;
    let productTimer: ReturnType<typeof setTimeout> | null = null;
    let hideTimer: ReturnType<typeof setTimeout> | null = null;

    if (isActive) {
      setShowProduct(true);

      lineProgress.stopAnimation();
      productProgress.stopAnimation();

      lineProgress.setValue(0);
      productProgress.setValue(0);

      lineTimer = setTimeout(() => {
        Animated.timing(lineProgress, {
          toValue: 1,
          duration: 250,
          easing: Easing.out(Easing.quad),
          useNativeDriver: false,
        }).start();
      }, 300);

      productTimer = setTimeout(() => {
        Animated.timing(productProgress, {
          toValue: 1,
          duration: 400,
          easing: Easing.out(Easing.quad),
          useNativeDriver: true,
        }).start();
      }, 550);
    } else {
      Animated.timing(productProgress, {
        toValue: 0,
        duration: 200,
        easing: Easing.inOut(Easing.quad),
        useNativeDriver: true,
      }).start();

      lineTimer = setTimeout(() => {
        Animated.timing(lineProgress, {
          toValue: 0,
          duration: 180,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: false,
        }).start();
      }, 100);

      hideTimer = setTimeout(() => {
        setShowProduct(false);
      }, 300);
    }

    return () => {
      if (lineTimer) clearTimeout(lineTimer);
      if (productTimer) clearTimeout(productTimer);
      if (hideTimer) clearTimeout(hideTimer);
    };
  }, [isActive, lineProgress, productProgress]);

  const idleScale = idlePulse.interpolate({
    inputRange: [0, 1],
    outputRange: [1, 1.08],
  });

  const combinedScale = Animated.multiply(idleScale, activeScale);

  const glowScale = activeGlow.interpolate({
    inputRange: [0, 1],
    outputRange: [1, 1.1],
  });

  const glowOpacity = activeGlow.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 0.4],
  });

  const glowBackdropScale = activeGlow.interpolate({
    inputRange: [0, 1],
    outputRange: [0.8, 1],
  });

  const productScale = productProgress.interpolate({
    inputRange: [0, 1],
    outputRange: [0.3, 1],
  });

  const productOpacity = productProgress.interpolate({
    inputRange: [0, 0.3, 1],
    outputRange: [0, 0.4, 1],
  });

  // Animate width instead of scaleX.
  // This avoids needing transformOrigin and draws from the hotspot outward.
  const animatedLineWidth = lineProgress.interpolate({
    inputRange: [0, 1],
    outputRange: [0, lineWidth],
  });

  const lineOpacity = lineProgress.interpolate({
    inputRange: [0, 0.2, 1],
    outputRange: [0, 1, 1],
  });

  return (
    <View
      style={[
        styles.overlayContainer,
        {
          left: contentX,
          top: contentY,
        },
      ]}
      pointerEvents="box-none"
    >
      <Pressable
        onPress={() => onHotspotPress(index)}
        style={styles.hotspotPressable}
      >
        <View style={styles.hotspotWrapper}>
          <View style={styles.ringsContainer}>
            {[...Array(RING_COUNT)].map((_, i) => (
              <PulseRing
                key={i}
                delay={i * 400}
                isActive={isActive}
              />
            ))}
          </View>

          <Animated.View
            style={[
              styles.hotspotGlow,
              {
                opacity: glowOpacity,
                transform: [{ scale: glowBackdropScale }],
              },
            ]}
          />

          <Animated.View
            style={[
              styles.hotspotOuter,
              {
                transform: [{ scale: combinedScale }],
              },
              isActive && styles.hotspotOuterActive,
            ]}
          >
            <Animated.View
              style={[
                styles.hotspotInner,
                {
                  transform: [{ scale: glowScale }],
                },
                isActive && styles.hotspotInnerActive,
              ]}
            />
          </Animated.View>
        </View>
      </Pressable>

      {showProduct && (
        <Animated.View
          style={[
            styles.connectionLine,
            {
              width: animatedLineWidth,
              opacity: lineOpacity,
              top: -1,
              ...(isLeft ? styles.connectionLineLeft : styles.connectionLineRight),
            },
          ]}
        />
      )}

      {showProduct && (
        <Animated.View
          style={[
            styles.productContainer,
            {
              left: productX,
              top: productY,
              opacity: productOpacity,
              transform: [{ scale: productScale }],
            },
          ]}
        >
          <Actionable
            action={product?.actions?.openDetailsPage}
            hapticFeedback="impactLight"
            pressEffect="pop"
          >
            <Flexbox flexDirection="column" alignItems="center" gap={5}>
              <Image
                src={product?.images?.[0]}
                style={styles.productImage}
                resizeMode="cover"
              />

              {product?.priceRange?.minVariantPrice && (
                <View
                  bg="gray-1100/600"
                  px="4xs"
                  py="6xs"
                  borderRadius={10}
                >
                  <PriceDisplay
                    adjustsFontSizeToFit
                    numberOfLines={1}
                    color="gray-0"
                    fontSize="2xs"
                    price={product.priceRange.minVariantPrice}
                  />
                </View>
              )}
            </Flexbox>
          </Actionable>
        </Animated.View>
      )}
    </View>
  );
};

// ---------------------------------------------------------
// Navigation button
// ---------------------------------------------------------

interface NavButtonProps {
  direction: 'prev' | 'next';
  onPress: () => void;
  disabled?: boolean;
}

const NavButton: React.FC<NavButtonProps> = ({
  direction,
  onPress,
  disabled,
}) => {
  const scale = React.useRef(new Animated.Value(1)).current;

  const handlePressIn = () => {
    Animated.timing(scale, {
      toValue: 0.9,
      duration: 100,
      useNativeDriver: true,
    }).start();
  };

  const handlePressOut = () => {
    Animated.timing(scale, {
      toValue: 1,
      duration: 100,
      useNativeDriver: true,
    }).start();
  };

  return (
    <Pressable
      onPress={onPress}
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      disabled={disabled}
      style={[styles.navButton, disabled && styles.navButtonDisabled]}
    >
      <Animated.View
        style={[
          styles.navButtonInner,
          {
            transform: [{ scale }],
          },
        ]}
      >
        <Icon
          color="white"
          icon={
            direction === 'prev'
              ? 'evilicons:chevron-left'
              : 'evilicons:chevron-right'
          }
        />
      </Animated.View>
    </Pressable>
  );
};

// ---------------------------------------------------------
// Image
// ---------------------------------------------------------

interface LookImageProps {
  image: ImageData;
  imageIndex: number;
  layout: { width: number; height: number };
  activeHotspot: number;
  showCard: boolean;
  onHotspotPress: (index: number) => void;
  opacity: Animated.Value;
  isVisible: boolean;
  isOverlay?: boolean;
}

const LookImage: React.FC<LookImageProps> = ({
  image,
  imageIndex,
  layout,
  activeHotspot,
  showCard,
  onHotspotPress,
  opacity,
  isVisible,
  isOverlay = false,
}) => {
  if (!isVisible) return null;

  return (
    <Animated.View
      style={[
        isOverlay ? styles.lookImageOverlay : styles.lookImageBase,
        { opacity },
      ]}
    >
      <Image
        source={{ uri: image.url }}
        style={{ width: '100%', aspectRatio: image.aspectRatio }}
        resizeMode="cover"
      />

      <View style={styles.vignetteOverlay} />

      {layout.width > 0 &&
        image.annotations?.map((annotation, index) => (
          <HotspotWithCard
            key={`${imageIndex}-${annotation.id}`}
            annotation={annotation}
            index={index}
            imageWidth={layout.width}
            imageHeight={layout.height}
            isActive={index === activeHotspot && showCard}
            onHotspotPress={onHotspotPress}
          />
        ))}
    </Animated.View>
  );
};

// ---------------------------------------------------------
// App block
// ---------------------------------------------------------

const AppBlock: React.FC<AppBlockProps> = ({ data }) => {
  const { images } = data || {};

  if (!images || images.length === 0) {
    return (
      <View style={styles.emptyContainer}>
        <Text style={styles.emptyText}>No images available</Text>
      </View>
    );
  }

  const [currentImageIndex, setCurrentImageIndex] = React.useState(0);
  const [nextImageIndex, setNextImageIndex] = React.useState<number | null>(null);
  const [containerWidth, setContainerWidth] = React.useState(0);
  const [activeHotspot, setActiveHotspot] = React.useState(0);
  const [showCard, setShowCard] = React.useState(true);
  const [isTransitioning, setIsTransitioning] = React.useState(false);

  const currentOpacity = React.useRef(new Animated.Value(1)).current;
  const nextOpacity = React.useRef(new Animated.Value(0)).current;
  const animatedHeight = React.useRef(new Animated.Value(0)).current;

  const currentImage = images[currentImageIndex];
  const nextImage =
    nextImageIndex !== null ? images[nextImageIndex] : null;

  const totalImages = images.length;
  const totalHotspots = currentImage?.annotations?.length || 0;

  const getHeightForImage = React.useCallback(
    (image: ImageData, width: number) => {
      if (!width || !image?.aspectRatio) return 0;
      return width / image.aspectRatio;
    },
    [],
  );

  React.useEffect(() => {
    if (containerWidth > 0 && currentImage) {
      const initialHeight = getHeightForImage(
        currentImage,
        containerWidth,
      );
      animatedHeight.setValue(initialHeight);
    }
  }, [
    animatedHeight,
    containerWidth,
    currentImage,
    currentImage?.aspectRatio,
    getHeightForImage,
  ]);

  const layout = React.useMemo(
    () => ({
      width: containerWidth,
      height:
        containerWidth > 0 && currentImage
          ? getHeightForImage(currentImage, containerWidth)
          : 0,
    }),
    [containerWidth, currentImage, getHeightForImage],
  );

  const DISPLAY_MS = 3500;
  const TRANSITION_MS = 300;

  const cycleRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const transitionRef =
    React.useRef<ReturnType<typeof setTimeout> | null>(null);

  const clearTimers = React.useCallback(() => {
    if (cycleRef.current) {
      clearTimeout(cycleRef.current);
      cycleRef.current = null;
    }

    if (transitionRef.current) {
      clearTimeout(transitionRef.current);
      transitionRef.current = null;
    }
  }, []);

  const startCycle = React.useCallback(() => {
    clearTimers();

    cycleRef.current = setTimeout(() => {
      setShowCard(false);

      transitionRef.current = setTimeout(() => {
        setActiveHotspot((prev) =>
          totalHotspots > 0 ? (prev + 1) % totalHotspots : 0,
        );
        setShowCard(true);
        startCycle();
      }, TRANSITION_MS);
    }, DISPLAY_MS);
  }, [clearTimers, totalHotspots]);

  React.useEffect(() => {
    if (!totalHotspots || isTransitioning) return;

    startCycle();

    return () => clearTimers();
  }, [
    clearTimers,
    currentImageIndex,
    isTransitioning,
    startCycle,
    totalHotspots,
  ]);

  const handleHotspotPress = React.useCallback(
    (index: number) => {
      if (index === activeHotspot || isTransitioning) return;

      clearTimers();
      setShowCard(false);

      transitionRef.current = setTimeout(() => {
        setActiveHotspot(index);
        setShowCard(true);
        startCycle();
      }, TRANSITION_MS);
    },
    [
      activeHotspot,
      clearTimers,
      isTransitioning,
      startCycle,
    ],
  );

  const transitionToImage = React.useCallback(
    (targetIndex: number) => {
      if (isTransitioning || targetIndex === currentImageIndex) return;
      if (targetIndex < 0 || targetIndex >= totalImages) return;

      clearTimers();
      setShowCard(false);
      setIsTransitioning(true);
      setNextImageIndex(targetIndex);

      const targetImage = images[targetIndex];
      const targetHeight = getHeightForImage(
        targetImage,
        containerWidth,
      );

      currentOpacity.setValue(1);
      nextOpacity.setValue(0);

      Animated.parallel([
        Animated.timing(nextOpacity, {
          toValue: 1,
          duration: IMAGE_TRANSITION_MS,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: true,
        }),
        Animated.timing(animatedHeight, {
          toValue: targetHeight,
          duration: IMAGE_TRANSITION_MS,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: false,
        }),
      ]).start();

      const completeTimer = setTimeout(() => {
        setCurrentImageIndex(targetIndex);
        setActiveHotspot(0);
        setShowCard(true);
        setIsTransitioning(false);
      }, IMAGE_TRANSITION_MS);

      const clearOverlayTimer = setTimeout(() => {
        setNextImageIndex(null);
        nextOpacity.setValue(0);
      }, IMAGE_TRANSITION_MS + 100);

      return () => {
        clearTimeout(completeTimer);
        clearTimeout(clearOverlayTimer);
      };
    },
    [
      animatedHeight,
      clearTimers,
      containerWidth,
      currentImageIndex,
      currentOpacity,
      getHeightForImage,
      images,
      isTransitioning,
      nextOpacity,
      totalImages,
    ],
  );

  const handlePrevImage = React.useCallback(() => {
    const prevIndex =
      currentImageIndex === 0
        ? totalImages - 1
        : currentImageIndex - 1;

    transitionToImage(prevIndex);
  }, [currentImageIndex, totalImages, transitionToImage]);

  const handleNextImage = React.useCallback(() => {
    const nextIndex =
      currentImageIndex === totalImages - 1
        ? 0
        : currentImageIndex + 1;

    transitionToImage(nextIndex);
  }, [currentImageIndex, totalImages, transitionToImage]);

  const hasInitializedHeight = React.useRef(false);

  const handleLayout = React.useCallback(
    (e: any) => {
      const { width } = e.nativeEvent.layout;

      if (width !== containerWidth) {
        setContainerWidth(width);

        if (currentImage && !hasInitializedHeight.current) {
          hasInitializedHeight.current = true;
          animatedHeight.setValue(
            getHeightForImage(currentImage, width),
          );
        }
      }
    },
    [
      animatedHeight,
      containerWidth,
      currentImage,
      getHeightForImage,
    ],
  );

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.imageWrapper,
          {
            height: animatedHeight,
          },
        ]}
        onLayout={handleLayout}
      >
        <LookImage
          image={currentImage}
          imageIndex={currentImageIndex}
          layout={layout}
          activeHotspot={activeHotspot}
          showCard={showCard && !isTransitioning}
          onHotspotPress={handleHotspotPress}
          opacity={currentOpacity}
          isVisible
          isOverlay={false}
        />

        {nextImage && (
          <LookImage
            image={nextImage}
            imageIndex={nextImageIndex!}
            layout={{
              width: containerWidth,
              height: getHeightForImage(
                nextImage,
                containerWidth,
              ),
            }}
            activeHotspot={0}
            showCard={false}
            onHotspotPress={() => {}}
            opacity={nextOpacity}
            isVisible
            isOverlay
          />
        )}

        {totalImages > 1 && (
          <>
            <View style={styles.navButtonLeft}>
              <NavButton
                direction="prev"
                onPress={handlePrevImage}
                disabled={isTransitioning}
              />
            </View>

            <View style={styles.navButtonRight}>
              <NavButton
                direction="next"
                onPress={handleNextImage}
                disabled={isTransitioning}
              />
            </View>
          </>
        )}
      </Animated.View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'transparent',
  },
  emptyContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 24,
    backgroundColor: 'transparent',
  },
  emptyText: {
    fontSize: 14,
    color: '#666',
    textAlign: 'center',
  },
  imageWrapper: {
    width: '100%',
    position: 'relative',
    zIndex: 1,
    overflow: 'hidden',
  },
  lookImageBase: {
    position: 'relative',
    width: '100%',
  },
  lookImageOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    width: '100%',
  },
  vignetteOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'transparent',
    borderWidth: 0,
  },
  overlayContainer: {
    position: 'absolute',
    alignItems: 'center',
    justifyContent: 'center',
    zIndex: 10,
    overflow: 'visible',
  },
  ringsContainer: {
    position: 'absolute',
    width: HOTSPOT_SIZE,
    height: HOTSPOT_SIZE,
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 5,
  },
  pulseRing: {
    position: 'absolute',
    width: HOTSPOT_SIZE,
    height: HOTSPOT_SIZE,
    borderRadius: HOTSPOT_SIZE / 2,
    borderWidth: 2,
    borderColor: ACTIVE_COLOR,
    backgroundColor: 'transparent',
  },
  hotspotPressable: {
    width: HOTSPOT_SIZE * 2,
    height: HOTSPOT_SIZE * 2,
    marginLeft: -HOTSPOT_SIZE,
    marginTop: -HOTSPOT_SIZE,
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 15,
  },
  hotspotWrapper: {
    width: HOTSPOT_SIZE,
    height: HOTSPOT_SIZE,
    justifyContent: 'center',
    alignItems: 'center',
  },
  hotspotGlow: {
    position: 'absolute',
    width: HOTSPOT_SIZE * 2,
    height: HOTSPOT_SIZE * 2,
    borderRadius: HOTSPOT_SIZE,
    backgroundColor: ACTIVE_COLOR,
  },
  hotspotOuter: {
    width: HOTSPOT_SIZE,
    height: HOTSPOT_SIZE,
    borderRadius: HOTSPOT_SIZE / 2,
    backgroundColor: 'rgba(255, 255, 255, 0.2)',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1.5,
    borderColor: 'rgba(255, 255, 255, 0.6)',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    elevation: 4,
  },
  hotspotInner: {
    width: HOTSPOT_SIZE * 0.45,
    height: HOTSPOT_SIZE * 0.45,
    borderRadius: HOTSPOT_SIZE * 0.225,
    backgroundColor: '#fff',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
  },
  hotspotOuterActive: {
    borderColor: ACTIVE_COLOR,
    backgroundColor: 'rgba(76, 217, 100, 0.15)',
  },
  hotspotInnerActive: {
    backgroundColor: ACTIVE_COLOR,
  },
  connectionLine: {
    position: 'absolute',
    height: 2,
    backgroundColor: ACTIVE_COLOR,
    borderRadius: 1,
    zIndex: 12,
  },
  // The hotspot press target is 2x HOTSPOT_SIZE and is centered using
  // negative margins. Offset the line by HOTSPOT_SIZE so its inner edge
  // aligns with the actual hotspot center rather than the pressable edge.
  connectionLineLeft: {
    right: HOTSPOT_SIZE,
  },
  connectionLineRight: {
    left: HOTSPOT_SIZE,
  },
  productContainer: {
    position: 'absolute',
    width: IMAGE_SIZE,
    alignItems: 'center',
    zIndex: 20,
  },
  productImage: {
    width: IMAGE_SIZE,
    height: IMAGE_SIZE,
    borderRadius: IMAGE_SIZE / 2,
    borderWidth: 2,
    borderColor: '#fff',
    backgroundColor: '#f0f0f0',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.25,
    shadowRadius: 6,
    elevation: 8,
  },
  navButtonLeft: {
    position: 'absolute',
    left: 12,
    top: '50%',
    marginTop: -NAV_BUTTON_SIZE / 2,
    zIndex: 30,
  },
  navButtonRight: {
    position: 'absolute',
    right: 12,
    top: '50%',
    marginTop: -NAV_BUTTON_SIZE / 2,
    zIndex: 30,
  },
  navButton: {
    width: NAV_BUTTON_SIZE,
    height: NAV_BUTTON_SIZE,
    borderRadius: NAV_BUTTON_SIZE / 2,
    justifyContent: 'center',
    alignItems: 'center',
  },
  navButtonDisabled: {
    opacity: 0.3,
  },
  navButtonInner: {
    width: NAV_BUTTON_SIZE,
    height: NAV_BUTTON_SIZE,
    borderRadius: NAV_BUTTON_SIZE / 2,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'rgba(255, 255, 255, 0.3)',
  },
});

export default AppBlock;