{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"marketing-gallery-fullscreen-gallery","type":"registry:block","title":"Fullscreen Gallery","files":[{"path":"components/blocks/marketing/gallery/fullscreen-gallery.tsx","content":"import * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ExpandIcon,\n  MinimizeIcon,\n} from 'lucide-react';\nimport { Button } from '@/components/ui/button';\n\ninterface GalleryImage {\n  src: string;\n  alt: string;\n  width: number;\n  height: number;\n}\n\nexport default function FullscreenGallery() {\n  const [currentIndex, setCurrentIndex] = React.useState(0);\n  const [isFullscreen, setIsFullscreen] = React.useState(false);\n  const galleryRef = React.useRef<HTMLDivElement>(null);\n\n  // Static images array\n  const images: GalleryImage[] = [\n    {\n      src: 'https://images.unsplash.com/photo-1682687982501-1e58ab814714?q=80&w=1470&auto=format&fit=crop',\n      alt: 'Emerald lake surrounded by forest and mountains',\n      width: 1470,\n      height: 980,\n    },\n    {\n      src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=1470&auto=format&fit=crop',\n      alt: 'Majestic mountain peaks with snow and clouds',\n      width: 1470,\n      height: 980,\n    },\n    {\n      src: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?q=80&w=1374&auto=format&fit=crop',\n      alt: 'Powerful waterfall flowing through lush greenery',\n      width: 1374,\n      height: 916,\n    },\n    {\n      src: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=1474&auto=format&fit=crop',\n      alt: 'Foggy forest with sunlight streaming through trees',\n      width: 1474,\n      height: 982,\n    },\n    {\n      src: 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?q=80&w=1470&auto=format&fit=crop',\n      alt: 'Serene mountain lake reflecting snow-capped peaks',\n      width: 1470,\n      height: 980,\n    },\n  ];\n\n  const prevSlide = () => {\n    setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));\n  };\n\n  const nextSlide = () => {\n    setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));\n  };\n\n  // Handle fullscreen toggling\n  const toggleFullscreen = () => {\n    if (!isFullscreen) {\n      if (galleryRef.current?.requestFullscreen) {\n        galleryRef.current.requestFullscreen();\n      }\n      setIsFullscreen(true);\n    } else {\n      if (document.exitFullscreen) {\n        document.exitFullscreen();\n      }\n      setIsFullscreen(false);\n    }\n  };\n\n  React.useEffect(() => {\n    const handleFullscreenChange = () => {\n      setIsFullscreen(!!document.fullscreenElement);\n    };\n\n    document.addEventListener('fullscreenchange', handleFullscreenChange);\n    return () => {\n      document.removeEventListener('fullscreenchange', handleFullscreenChange);\n    };\n  }, []);\n\n  // Keyboard navigation\n  React.useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'ArrowRight') {\n        nextSlide();\n      } else if (e.key === 'ArrowLeft') {\n        prevSlide();\n      } else if (e.key === 'Escape' && isFullscreen) {\n        toggleFullscreen();\n      } else if (e.key === 'f') {\n        toggleFullscreen();\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [isFullscreen]);\n\n  return (\n    <div\n      ref={galleryRef}\n      className={cn(\n        'relative w-full overflow-hidden bg-black',\n        isFullscreen ? 'h-screen' : 'aspect-video rounded-lg'\n      )}\n    >\n      {/* Progress bar */}\n      <div className=\"absolute top-0 right-0 left-0 z-10 h-1 bg-black/20\">\n        <div\n          className=\"h-full bg-white\"\n          style={{ width: `${((currentIndex + 1) / images.length) * 100}%` }}\n        />\n      </div>\n\n      {/* Images */}\n      <div className=\"relative h-full w-full\">\n        {images.map((image, index) => (\n          <div\n            key={`slide-${index}`}\n            className={cn(\n              'absolute inset-0 transition-all duration-500',\n              index === currentIndex ? 'opacity-100' : 'opacity-0'\n            )}\n          >\n            <img\n              src={image.src}\n              alt={image.alt}\n              sizes=\"100vw\"\n              className=\"object-contain\"\n            />\n          </div>\n        ))}\n      </div>\n\n      {/* Controls - centered at the bottom */}\n      <div className=\"absolute inset-x-0 bottom-0 z-10 flex items-center justify-between p-4 text-white\">\n        <div className=\"text-xs text-white/70\">\n          {currentIndex + 1} / {images.length}\n        </div>\n\n        <div className=\"mb-2 flex items-center gap-2\">\n          <Button size=\"icon\" onClick={prevSlide}>\n            <ChevronLeftIcon className=\"h-5 w-5\" />\n          </Button>\n\n          <Button size=\"icon\" onClick={toggleFullscreen}>\n            {isFullscreen ? (\n              <MinimizeIcon className=\"h-5 w-5\" />\n            ) : (\n              <ExpandIcon className=\"h-5 w-5\" />\n            )}\n          </Button>\n\n          <Button size=\"icon\" onClick={nextSlide}>\n            <ChevronRightIcon className=\"h-5 w-5\" />\n          </Button>\n        </div>\n\n        <div className=\"mb-3 max-w-md text-center\">\n          <p className=\"sr-only\">{images[currentIndex]!.alt}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/blocks/marketing/gallery/fullscreen-gallery.tsx"},{"path":"components/ui/button.tsx","content":"import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/80\",\n        outline:\n          \"border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n        ghost:\n          \"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50\",\n        destructive:\n          \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default:\n          \"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        xs: \"h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5\",\n        lg: \"h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        icon: \"size-9\",\n        \"icon-xs\":\n          \"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\":\n          \"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Button, buttonVariants }\n","type":"registry:ui","target":"components/ui/button.tsx"}],"description":"A fullscreen gallery that fills the viewport with one image at a time. Good for immersive photo stories, lookbooks, or a striking visual showcase.","dependencies":["class-variance-authority","lucide-react","radix-ui"]}