{
  "name": "product-image-gallery",
  "title": "ProductImageGallery",
  "description": "Main image with thumbnail strip for product images.",
  "type": "component",
  "registryDependencies": [],
  "files": [
    {
      "path": "product-image-gallery.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport { preloadImage } from \"./utils/preload-image\";\n\nexport interface ProductImageGalleryProps {\n  images: string[];\n  productName: string;\n  aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n  className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n  square: { aspectRatio: \"1/1\" },\n  \"4/3\": { aspectRatio: \"4/3\" },\n  \"16/10\": { aspectRatio: \"16/10\" },\n  \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n  images,\n  productName,\n  aspectRatio = \"4/3\",\n  className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n  const normalizedImages = useMemo(\n    () =>\n      images.filter(\n        (image): image is string =>\n          typeof image === \"string\" && image.trim().length > 0,\n      ),\n    [images],\n  );\n\n  const [selectedImage, setSelectedImage] = useState(0);\n  const [fadeIn, setFadeIn] = useState(true);\n\n  useEffect(() => {\n    setSelectedImage(0);\n  }, [normalizedImages.length, productName]);\n\n  // Cross-fade the main image whenever the selection changes (thumbnail click\n  // or a variant swapping in its own gallery).\n  useEffect(() => {\n    setFadeIn(false);\n    const id = requestAnimationFrame(() => setFadeIn(true));\n    return () => cancelAnimationFrame(id);\n  }, [selectedImage]);\n\n  if (normalizedImages.length === 0) {\n    return null;\n  }\n\n  const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n  preloadImage(normalizedImages[selectedImage + 1]);\n  preloadImage(normalizedImages[selectedImage - 1]);\n\n  return (\n    <div data-cimplify-image-gallery className={className}>\n      <div\n        data-cimplify-image-gallery-main\n        style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n      >\n        <img\n          src={activeImage}\n          alt={productName}\n          fetchPriority=\"high\"\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            objectFit: \"cover\",\n            opacity: fadeIn ? 1 : 0,\n            transition: \"opacity 300ms ease\",\n          }}\n          data-cimplify-image-gallery-active\n        />\n      </div>\n\n      {normalizedImages.length > 1 && (\n        <RadioGroup\n          aria-label={`${productName} image thumbnails`}\n          value={String(selectedImage)}\n          onValueChange={(value) => setSelectedImage(Number(value))}\n          data-cimplify-image-gallery-thumbnails\n          style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}\n        >\n          {normalizedImages.map((image, index) => {\n            const isSelected = selectedImage === index;\n            return (\n              <Radio.Root\n                key={`${image}-${index}`}\n                value={String(index)}\n                                data-cimplify-image-gallery-thumb\n                data-selected={isSelected || undefined}\n                style={{\n                  width: \"4rem\",\n                  height: \"4rem\",\n                  overflow: \"hidden\",\n                  padding: 0,\n                  border: \"none\",\n                  cursor: \"pointer\",\n                }}\n              >\n                <img\n                  src={image}\n                  alt=\"\"\n                  loading=\"lazy\"\n                  decoding=\"async\"\n                  style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n                />\n              </Radio.Root>\n            );\n          })}\n        </RadioGroup>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}
