{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"marketing-gallery-filmstrip-gallery","type":"registry:block","title":"Filmstrip Gallery","files":[{"path":"components/blocks/marketing/gallery/filmstrip-gallery.tsx","content":"import * as React from 'react';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\n\ninterface FilmstripImage {\n  src: string;\n  alt: string;\n  width: number;\n  height: number;\n}\n\nexport default function FilmstripGallery() {\n  const scrollContainerRef = React.useRef<HTMLDivElement>(null);\n  const [showLeftArrow, setShowLeftArrow] = React.useState(false);\n  const [showRightArrow, setShowRightArrow] = React.useState(true);\n\n  // Static images array with film/photography theme\n  const images = React.useMemo<FilmstripImage[]>(\n    () => [\n      {\n        src: 'https://images.unsplash.com/photo-1495360010541-f48722b34f7d?q=80&w=1476&auto=format&fit=crop',\n        alt: 'Vintage film camera with leather case',\n        width: 1476,\n        height: 984,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1581591524425-c7e0978865fc?q=80&w=1470&auto=format&fit=crop',\n        alt: 'Collection of film rolls and negatives',\n        width: 1470,\n        height: 980,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1606131731446-5568d87113aa?q=80&w=1528&auto=format&fit=crop',\n        alt: '35mm camera with film roll',\n        width: 1528,\n        height: 1019,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1587614313085-5da51cebd8ac?q=80&w=1470&auto=format&fit=crop',\n        alt: 'Vintage photo prints spread out',\n        width: 1470,\n        height: 980,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1607462109225-6b64ae2dd3cb?q=80&w=1374&auto=format&fit=crop',\n        alt: 'Person developing film in darkroom',\n        width: 1374,\n        height: 916,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1515634928627-2a4e0dae3ddf?q=80&w=1470&auto=format&fit=crop',\n        alt: 'Stack of vintage Polaroid photos',\n        width: 1470,\n        height: 980,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1516035069371-29a1b244cc32?q=80&w=1528&auto=format&fit=crop',\n        alt: 'Photographer adjusting vintage film camera',\n        width: 1528,\n        height: 1019,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1519458246479-6acae7536988?q=80&w=1470&auto=format&fit=crop',\n        alt: 'Developed film negatives on light table',\n        width: 1470,\n        height: 980,\n      },\n      {\n        src: 'https://images.unsplash.com/photo-1635315619556-5826839a1bea?q=80&w=1470&auto=format&fit=crop',\n        alt: 'Retro movie projector in action',\n        width: 1470,\n        height: 980,\n      },\n    ],\n    []\n  );\n\n  // Handle scroll arrows visibility\n  const handleScroll = React.useCallback(() => {\n    if (!scrollContainerRef.current) return;\n\n    const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;\n\n    // Show left arrow if scrolled right\n    setShowLeftArrow(scrollLeft > 20);\n\n    // Hide right arrow if reached the end\n    setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 20);\n  }, []);\n\n  // Attach scroll listener\n  React.useEffect(() => {\n    const scrollContainer = scrollContainerRef.current;\n    if (scrollContainer) {\n      scrollContainer.addEventListener('scroll', handleScroll);\n      // Initial check\n      handleScroll();\n\n      return () => {\n        scrollContainer.removeEventListener('scroll', handleScroll);\n      };\n    }\n  }, [handleScroll]);\n\n  // Scroll functions\n  const scrollLeft = () => {\n    if (scrollContainerRef.current) {\n      scrollContainerRef.current.scrollBy({\n        left: -300,\n        behavior: 'smooth',\n      });\n    }\n  };\n\n  const scrollRight = () => {\n    if (scrollContainerRef.current) {\n      scrollContainerRef.current.scrollBy({\n        left: 300,\n        behavior: 'smooth',\n      });\n    }\n  };\n\n  return (\n    <div className=\"w-full p-4 md:p-6\">\n      <div className=\"relative\">\n        {/* Title */}\n        <div className=\"mb-6 text-center\">\n          <h2 className=\"text-2xl font-bold\">Film Collection</h2>\n          <p className=\"text-muted-foreground mt-2\">\n            Scroll to explore the gallery →\n          </p>\n        </div>\n\n        {/* Scroll controls */}\n        {showLeftArrow && (\n          <Button\n            variant=\"secondary\"\n            size=\"icon\"\n            className=\"absolute top-1/2 -left-3 z-10 h-10 w-10 -translate-y-1/2 rounded-full shadow-md md:-left-5\"\n            onClick={scrollLeft}\n            aria-label=\"Scroll left\"\n          >\n            <ChevronLeft className=\"h-5 w-5\" />\n          </Button>\n        )}\n\n        {showRightArrow && (\n          <Button\n            variant=\"secondary\"\n            size=\"icon\"\n            className=\"absolute top-1/2 -right-3 z-10 h-10 w-10 -translate-y-1/2 rounded-full shadow-md md:-right-5\"\n            onClick={scrollRight}\n            aria-label=\"Scroll right\"\n          >\n            <ChevronRight className=\"h-5 w-5\" />\n          </Button>\n        )}\n\n        {/* Filmstrip container */}\n        <div\n          ref={scrollContainerRef}\n          className=\"no-scrollbar flex w-full overflow-x-auto pb-8\"\n          style={{ scrollbarWidth: 'none' }}\n        >\n          <div className=\"flex gap-5\">\n            {images.map((image, index) => (\n              <div\n                key={index}\n                className=\"group relative flex-shrink-0\"\n                style={{ width: '280px' }}\n              >\n                {/* Film frame */}\n                <div className=\"flex flex-col overflow-hidden rounded-lg bg-black pt-3 pb-5\">\n                  {/* Sprocket holes at top */}\n                  <div className=\"mb-3 flex justify-center gap-6 px-3\">\n                    {[...Array(4)].map((_, i) => (\n                      <div\n                        key={i}\n                        className=\"h-3 w-3 rounded-full border border-gray-500 bg-transparent\"\n                      />\n                    ))}\n                  </div>\n\n                  {/* Image */}\n                  <div className=\"relative aspect-[3/2] overflow-hidden\">\n                    <img\n                      src={image.src}\n                      alt={image.alt}\n                      sizes=\"280px\"\n                      className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n                    />\n                    <div className=\"absolute inset-0 border border-gray-800 shadow-inner\"></div>\n                  </div>\n\n                  {/* Sprocket holes at bottom */}\n                  <div className=\"mt-3 flex justify-center gap-6 px-3\">\n                    {[...Array(4)].map((_, i) => (\n                      <div\n                        key={i}\n                        className=\"h-3 w-3 rounded-full border border-gray-500 bg-transparent\"\n                      />\n                    ))}\n                  </div>\n\n                  {/* Frame number */}\n                  <div className=\"mt-2 text-center text-xs text-gray-400\">\n                    FRAME {String(index + 1).padStart(2, '0')}\n                  </div>\n                </div>\n\n                {/* Caption */}\n                <div className=\"mt-2 text-center text-sm\">{image.alt}</div>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/blocks/marketing/gallery/filmstrip-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 horizontal filmstrip of thumbnails with a larger preview above. Ideal for browsing a photo set or product angles while keeping every frame in view.","dependencies":["class-variance-authority","lucide-react","radix-ui"]}