{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gallery-lightbox",
  "type": "registry:ui",
  "title": "Gallery Lightbox",
  "description": "Click-to-expand image gallery with a fullscreen overlay, prev/next, and keyboard navigation.",
  "dependencies": [
    "@saasflare/ui",
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/gallery-lightbox.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n/**\n * @fileoverview Fullscreen image gallery lightbox with navigation.\n * @author Saasflare™\n * Opens images in a fullscreen overlay with prev/next navigation and\n * keyboard support. Supports images and video URLs.\n * @module packages/ui/components/ui/gallery-lightbox\n * @package ui\n *\n * @component\n * @example\n * import { GalleryLightbox } from '@saasflare/ui';\n * const [open, setOpen] = useState(false);\n * const [index, setIndex] = useState(0);\n * <GalleryLightbox\n *   images={[\"/img1.jpg\", \"/img2.jpg\", \"/img3.jpg\"]}\n *   open={open}\n *   index={index}\n *   onClose={() => setOpen(false)}\n *   onIndexChange={setIndex}\n * />\n */\n\nimport { useEffect, useCallback } from \"react\"\nimport { AnimatePresence, m } from \"motion/react\"\nimport { XIcon, CaretLeftIcon, CaretRightIcon } from \"@saasflare/ui\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { useSaasflareMotion } from \"@saasflare/ui\"\nimport { useFocusTrap } from \"@saasflare/ui\"\nimport { useScrollLock } from \"@saasflare/ui\"\n\n/** Props for the GalleryLightbox component. */\nexport interface GalleryLightboxProps extends SaasflareComponentProps {\n  /** Array of image URLs. */\n  images: string[]\n  /** Whether the lightbox is open. */\n  open: boolean\n  /** Current image index. */\n  index: number\n  /** Called when the lightbox should close. */\n  onClose: () => void\n  /** Called when the index changes. */\n  onIndexChange: (index: number) => void\n  /** Additional class names. */\n  className?: string\n}\n\n/**\n * Fullscreen image lightbox with keyboard navigation.\n *\n * @component\n * @package ui\n */\nexport function GalleryLightbox({\n  images,\n  open,\n  index,\n  onClose,\n  onIndexChange,\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: GalleryLightboxProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const motion = useSaasflareMotion(sf.animated, { duration: 0.2 })\n\n  // Modal a11y: trap focus inside the overlay (with focus restoration on close)\n  // and lock body scroll — same hooks HeroVideoDialog uses.\n  const overlayRef = useFocusTrap<HTMLDivElement>(open)\n  useScrollLock(open)\n\n  const prev = useCallback(() => {\n    onIndexChange((index - 1 + images.length) % images.length)\n  }, [index, images.length, onIndexChange])\n\n  const next = useCallback(() => {\n    onIndexChange((index + 1) % images.length)\n  }, [index, images.length, onIndexChange])\n\n  useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose()\n      if (e.key === \"ArrowLeft\") prev()\n      if (e.key === \"ArrowRight\") next()\n    }\n    window.addEventListener(\"keydown\", onKey)\n    return () => window.removeEventListener(\"keydown\", onKey)\n  }, [open, onClose, prev, next])\n\n  return (\n    <AnimatePresence>\n      {open && (\n        <m.div\n          ref={overlayRef}\n          initial={motion.disabled ? false : { opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={motion.transition}\n          className={cn(\n            \"fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm\",\n            className,\n          )}\n          onClick={onClose}\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-label=\"Image gallery\"\n          tabIndex={-1}\n          data-slot=\"gallery-lightbox\"\n          data-surface={sf.surface}\n          data-radius={sf.radius}\n          data-animated={String(sf.animated)}\n        >\n          {/* Close */}\n          <button\n            type=\"button\"\n            onClick={onClose}\n            className=\"absolute right-4 top-4 z-10 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20\"\n            aria-label=\"Close gallery\"\n          >\n            <XIcon weight={sf.iconWeight} className=\"size-6\" />\n          </button>\n\n          {/* Prev */}\n          {images.length > 1 && (\n            <button\n              type=\"button\"\n              onClick={(e) => { e.stopPropagation(); prev() }}\n              className=\"absolute left-4 z-10 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20\"\n              aria-label=\"Previous image\"\n            >\n              <CaretLeftIcon weight={sf.iconWeight} className=\"size-6\" />\n            </button>\n          )}\n\n          {/* Image */}\n          {images[index] && (\n            <m.img\n              key={index}\n              src={images[index]}\n              alt={`Image ${index + 1} of ${images.length}`}\n              initial={motion.disabled ? false : { opacity: 0, scale: 0.95 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={motion.disabled ? { opacity: 0 } : { opacity: 0, scale: 0.95 }}\n              transition={motion.transition}\n              className=\"max-h-[85vh] max-w-[90vw] rounded-lg object-contain\"\n              onClick={(e) => e.stopPropagation()}\n            />\n          )}\n\n          {/* Next */}\n          {images.length > 1 && (\n            <button\n              type=\"button\"\n              onClick={(e) => { e.stopPropagation(); next() }}\n              className=\"absolute right-4 z-10 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20\"\n              aria-label=\"Next image\"\n            >\n              <CaretRightIcon weight={sf.iconWeight} className=\"size-6\" />\n            </button>\n          )}\n\n          {/* Counter */}\n          {images.length > 1 && (\n            <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-sm text-white backdrop-blur-sm\">\n              {index + 1} / {images.length}\n            </div>\n          )}\n        </m.div>\n      )}\n    </AnimatePresence>\n  )\n}\n",
      "target": "components/ui/gallery-lightbox.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/gallery-lightbox.json"
  }
}
