{
  "name": "image-carousel-basic",
  "type": "registry:component",
  "dependencies": [
    "@radix-ui/react-dialog",
    "lucide-react",
    "embla-carousel-react",
    "embla-carousel",
    "react-zoom-pan-pinch"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n} from \"@radix-ui/react-dialog\";\nimport { type EmblaOptionsType } from \"embla-carousel\";\nimport useEmblaCarousel from \"embla-carousel-react\";\nimport {\n  ArrowLeft,\n  ArrowRight,\n  MinusCircle,\n  PlusCircle,\n  X,\n} from \"lucide-react\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { TransformComponent, TransformWrapper } from \"react-zoom-pan-pinch\";\n\ntype ThumbPropType = {\n  selected: boolean;\n  index: number;\n  onClick: () => void;\n  imgUrl: string;\n  title?: string;\n};\n\nconst getAspectRatioClass = (ratio?: string) => {\n  switch (ratio) {\n    case \"square\":\n      return \"aspect-square\"; // 1:1\n    case \"video\":\n      return \"aspect-video\"; // 16:9\n    case \"wide\":\n      return \"aspect-4/3\"; // 4:3\n    case \"auto\":\n      return \"aspect-auto\"; // Natural image aspect ratio\n    default:\n      return \"aspect-4/3\"; // Default 4:3\n  }\n};\n\nconst ImageContainer: React.FC<{\n  image: { url: string; title?: string };\n  alt: string;\n  fit?: \"cover\" | \"contain\" | \"fill\";\n  aspectRatio?: string;\n  showImageControls?: boolean;\n  classNameImage?: string;\n  classNameThumbnail?: string;\n}> = ({\n  alt,\n  aspectRatio,\n  classNameImage,\n  classNameThumbnail,\n  fit = \"cover\",\n  image,\n  showImageControls,\n}) => {\n  return (\n    <div\n      className={cn(\n        \"relative w-full overflow-hidden rounded-lg bg-gray-100\",\n        getAspectRatioClass(aspectRatio)\n      )}\n    >\n      <Dialog>\n        <DialogTrigger asChild>\n          <div className={`cursor-pointer`}>\n            <img\n              src={image.url}\n              alt={image.title || alt}\n              width={400}\n              height={600}\n              className={cn(\n                \"absolute inset-0 h-full w-full\",\n                fit === \"contain\" && \"object-contain\",\n                fit === \"cover\" && \"object-cover\",\n                fit === \"fill\" && \"object-fill\",\n                classNameThumbnail\n              )}\n            />\n          </div>\n        </DialogTrigger>\n\n        <DialogPortal>\n          <DialogOverlay className=\"fixed inset-0 z-50 bg-black/80\" />\n          <DialogContent className=\"bg-background fixed inset-0 z-50 flex flex-col items-center justify-center p-0\">\n            <DialogTitle className=\"sr-only\">\n              {image.title || \"Image\"}\n            </DialogTitle>\n            <DialogDescription className=\"sr-only\">\n              {image.title || \"Image\"}\n            </DialogDescription>\n\n            <div className=\"relative flex h-screen w-screen items-center justify-center\">\n              <TransformWrapper\n                initialScale={1}\n                initialPositionX={0}\n                initialPositionY={0}\n              >\n                {({ zoomIn, zoomOut }) => (\n                  <>\n                    <TransformComponent>\n                      {/* You can swap this with your preferred image optization technique, like using  next/image */}\n                      <img\n                        src={image.url}\n                        alt={image.title || \"Full size\"}\n                        className={cn(\n                          \"max-h-[90vh] max-w-[90vw] object-contain\",\n                          classNameImage\n                        )}\n                      />\n                    </TransformComponent>\n                    {showImageControls && (\n                      <div className=\"absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 gap-2\">\n                        <button\n                          onClick={() => zoomOut()}\n                          className=\"cursor-pointer rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70\"\n                          aria-label=\"Zoom out\"\n                        >\n                          <MinusCircle className=\"size-6\" />\n                        </button>\n                        <button\n                          onClick={() => zoomIn()}\n                          className=\"cursor-pointer rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70\"\n                          aria-label=\"Zoom in\"\n                        >\n                          <PlusCircle className=\"size-6\" />\n                        </button>\n                      </div>\n                    )}\n                  </>\n                )}\n              </TransformWrapper>\n              <DialogClose asChild>\n                <button\n                  className=\"absolute top-4 right-4 z-10 cursor-pointer rounded-full border bg-black/50 p-2 text-white transition-colors hover:bg-black/70\"\n                  aria-label=\"Close\"\n                >\n                  <X className=\"size-6\" />\n                </button>\n              </DialogClose>\n            </div>\n          </DialogContent>\n        </DialogPortal>\n      </Dialog>\n    </div>\n  );\n};\n\nconst Thumb: React.FC<ThumbPropType> = (props) => {\n  const { imgUrl, index, onClick, selected, title } = props;\n\n  return (\n    <div\n      className={cn(\n        \"transition-opacity duration-200\",\n        selected ? \"opacity-100\" : \"opacity-50 hover:opacity-70\",\n        // Horizontal layout (top/bottom)\n        \"group-[.thumbs-horizontal]:min-w-0 group-[.thumbs-horizontal]:flex-[0_0_22%] group-[.thumbs-horizontal]:pl-3 sm:group-[.thumbs-horizontal]:flex-[0_0_15%]\",\n        // Vertical layout (left/right)\n        \"group-[.thumbs-vertical]:w-full group-[.thumbs-vertical]:pt-3\"\n      )}\n    >\n      <button\n        onClick={onClick}\n        className=\"relative w-full cursor-pointer touch-manipulation appearance-none overflow-hidden rounded-md border-0 bg-transparent p-0\"\n        type=\"button\"\n      >\n        <div\n          className={cn(\n            \"relative w-full overflow-hidden rounded-lg bg-gray-100\",\n            getAspectRatioClass(\"square\")\n          )}\n        >\n          <img\n            src={imgUrl}\n            alt={title || `Thumbnail ${index + 1}`}\n            width={400}\n            height={600}\n            className={cn(\"h-full w-full object-cover\")}\n          />\n        </div>\n      </button>\n    </div>\n  );\n};\n\ntype CarouselImage = {\n  title?: string;\n  url: string;\n};\n\ntype CarouselImages = CarouselImage[];\ninterface ImageCarousel_BasicProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  images: CarouselImages;\n  opts?: EmblaOptionsType;\n  showCarouselControls?: boolean;\n  showImageControls?: boolean;\n  imageFit?: \"cover\" | \"contain\" | \"fill\";\n  aspectRatio?: \"square\" | \"video\" | \"wide\" | \"auto\";\n  thumbPosition?: \"bottom\" | \"top\" | \"left\" | \"right\";\n  showThumbs?: boolean;\n  // Controlled mode props\n  selectedIndex?: number;\n  onSlideChange?: (index: number) => void;\n  classNameImage?: string;\n  classNameThumbnail?: string;\n}\n\nconst ImageCarousel_Basic: React.FC<ImageCarousel_BasicProps> = ({\n  aspectRatio = \"wide\",\n  className,\n  classNameImage,\n  classNameThumbnail,\n  imageFit = \"contain\",\n  images,\n  onSlideChange,\n  opts,\n  // Controlled mode props\n  selectedIndex: controlledIndex,\n  showCarouselControls = true,\n  showImageControls = true,\n  showThumbs = true,\n  thumbPosition = \"bottom\",\n  ...props\n}) => {\n  const isControlled = controlledIndex !== undefined;\n\n  const [emblaRef, emblaApi] = useEmblaCarousel({\n    ...opts,\n    axis: \"x\",\n  });\n\n  const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel(\n    showThumbs\n      ? {\n          axis:\n            thumbPosition === \"left\" || thumbPosition === \"right\" ? \"y\" : \"x\",\n          containScroll: \"keepSnaps\",\n          dragFree: true,\n        }\n      : undefined\n  );\n\n  const onThumbClick = useCallback(\n    (index: number) => {\n      if (!emblaApi || !showThumbs || !emblaThumbsApi) return;\n\n      if (isControlled && onSlideChange) {\n        onSlideChange(index);\n      } else {\n        emblaApi.scrollTo(index);\n        emblaThumbsApi.scrollTo(index);\n      }\n    },\n    [emblaApi, emblaThumbsApi, showThumbs, isControlled, onSlideChange]\n  );\n\n  const [canScrollPrev, setCanScrollPrev] = useState(false);\n  const [canScrollNext, setCanScrollNext] = useState(false);\n  const [internalSelectedIndex, setInternalSelectedIndex] = useState(0);\n\n  // Use either controlled or internal state\n  const currentIndex = isControlled ? controlledIndex : internalSelectedIndex;\n\n  const scrollPrev = useCallback(() => {\n    if (isControlled && onSlideChange) {\n      const prevIndex = Math.max(0, currentIndex - 1);\n      onSlideChange(prevIndex);\n    } else if (emblaApi) {\n      emblaApi.scrollPrev();\n    }\n  }, [emblaApi, isControlled, onSlideChange, currentIndex]);\n\n  const scrollNext = useCallback(() => {\n    if (isControlled && onSlideChange && images) {\n      const nextIndex = Math.min(images.length - 1, currentIndex + 1);\n      onSlideChange(nextIndex);\n    } else if (emblaApi) {\n      emblaApi.scrollNext();\n    }\n  }, [emblaApi, isControlled, onSlideChange, currentIndex, images]);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        scrollPrev();\n      } else if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [scrollPrev, scrollNext]\n  );\n\n  const onSelect = useCallback(() => {\n    if (!emblaApi) return;\n\n    const selectedSlideIndex = emblaApi.selectedScrollSnap();\n    setCanScrollPrev(emblaApi.canScrollPrev());\n    setCanScrollNext(emblaApi.canScrollNext());\n\n    if (!isControlled) {\n      setInternalSelectedIndex(selectedSlideIndex);\n    } else if (onSlideChange && selectedSlideIndex !== controlledIndex) {\n      onSlideChange(selectedSlideIndex);\n    }\n\n    if (showThumbs && emblaThumbsApi) {\n      emblaThumbsApi.scrollTo(selectedSlideIndex);\n    }\n  }, [\n    emblaApi,\n    emblaThumbsApi,\n    showThumbs,\n    isControlled,\n    onSlideChange,\n    controlledIndex,\n  ]);\n\n  // Effect for controlled mode to update carousel position\n  useEffect(() => {\n    if (\n      isControlled &&\n      emblaApi &&\n      emblaApi.selectedScrollSnap() !== controlledIndex\n    ) {\n      emblaApi.scrollTo(controlledIndex);\n      if (showThumbs && emblaThumbsApi) {\n        emblaThumbsApi.scrollTo(controlledIndex);\n      }\n    }\n  }, [controlledIndex, emblaApi, emblaThumbsApi, isControlled, showThumbs]);\n\n  useEffect(() => {\n    if (!emblaApi) return;\n\n    onSelect();\n    emblaApi.on(\"reInit\", onSelect);\n    emblaApi.on(\"select\", onSelect);\n\n    return () => {\n      emblaApi.off(\"reInit\", onSelect);\n      emblaApi.off(\"select\", onSelect);\n    };\n  }, [emblaApi, onSelect]);\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full max-w-3xl\",\n        {\n          \"flex-row-reverse\": showThumbs && thumbPosition === \"left\",\n          \"flex gap-4\":\n            showThumbs &&\n            (thumbPosition === \"left\" || thumbPosition === \"right\"),\n        },\n        className\n      )}\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      onKeyDownCapture={handleKeyDown}\n      {...props}\n    >\n      {showThumbs && thumbPosition === \"top\" && (\n        <div className=\"mb-4\">\n          <div className=\"overflow-hidden\" ref={emblaThumbsRef}>\n            <div className=\"thumbs-horizontal group -ml-3 flex\">\n              {images?.map((image, index) => (\n                <Thumb\n                  key={index}\n                  onClick={() => onThumbClick(index)}\n                  selected={index === currentIndex}\n                  index={index}\n                  imgUrl={image.url}\n                  title={image.title}\n                />\n              ))}\n            </div>\n          </div>\n        </div>\n      )}\n\n      <div\n        className={cn(\n          \"relative\",\n          showThumbs &&\n            (thumbPosition === \"left\" || thumbPosition === \"right\") &&\n            \"flex-[1_1_75%]\"\n        )}\n        aria-label=\"Image carousel controls\"\n      >\n        <div ref={emblaRef} className=\"overflow-hidden rounded-lg\">\n          <div className=\"-ml-4 flex\">\n            {images?.map((image, index) => (\n              <div\n                key={index}\n                className=\"min-w-0 shrink-0 grow-0 basis-full pl-4\"\n                role=\"group\"\n                aria-roledescription=\"slide\"\n              >\n                <ImageContainer\n                  image={image}\n                  alt={`Slide ${index + 1}`}\n                  fit={imageFit}\n                  aspectRatio={aspectRatio}\n                  showImageControls={showImageControls}\n                  classNameImage={classNameImage}\n                  classNameThumbnail={classNameThumbnail}\n                />\n              </div>\n            ))}\n          </div>\n        </div>\n\n        {showCarouselControls && (\n          <>\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"bg-background/80 hover:bg-background dark:bg-background/80 dark:hover:bg-background absolute top-1/2 left-[2%] z-10 h-8 w-8 -translate-y-1/2 rounded-full backdrop-blur-xs disabled:opacity-50\"\n              disabled={!canScrollPrev}\n              onClick={scrollPrev}\n            >\n              <ArrowLeft className=\"h-4 w-4\" />\n              <span className=\"sr-only\">Previous slide</span>\n            </Button>\n\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"bg-background/80 hover:bg-background dark:bg-background/80 dark:hover:bg-background absolute top-1/2 right-[2%] z-10 h-8 w-8 -translate-y-1/2 rounded-full backdrop-blur-xs disabled:opacity-50\"\n              disabled={!canScrollNext}\n              onClick={scrollNext}\n            >\n              <ArrowRight className=\"h-4 w-4\" />\n              <span className=\"sr-only\">Next slide</span>\n            </Button>\n          </>\n        )}\n      </div>\n\n      {showThumbs &&\n        (thumbPosition === \"bottom\" ||\n          thumbPosition === \"left\" ||\n          thumbPosition === \"right\") && (\n          <div\n            className={cn(\n              thumbPosition === \"left\" || thumbPosition === \"right\"\n                ? \"relative flex-[0_0_20%]\"\n                : \"mt-4\"\n            )}\n          >\n            <div\n              className={cn(\n                \"overflow-hidden\",\n                (thumbPosition === \"left\" || thumbPosition === \"right\") &&\n                  \"absolute inset-0\"\n              )}\n              ref={emblaThumbsRef}\n            >\n              <div\n                className={cn(\n                  thumbPosition === \"bottom\"\n                    ? \"thumbs-horizontal group -ml-3 flex\"\n                    : \"thumbs-vertical group -mt-3 flex h-full flex-col\"\n                )}\n              >\n                {images?.map((image, index) => (\n                  <Thumb\n                    key={index}\n                    onClick={() => onThumbClick(index)}\n                    selected={index === currentIndex}\n                    index={index}\n                    imgUrl={image.url}\n                    title={image.title}\n                  />\n                ))}\n              </div>\n            </div>\n          </div>\n        )}\n    </div>\n  );\n};\n\nexport default ImageCarousel_Basic;\nexport type { CarouselImage, CarouselImages };\n",
      "path": "/components/commerce-ui/components/image-carousel/basic/image-carousel-basic.tsx",
      "target": "components/commerce-ui/image-carousel-basic.tsx"
    }
  ]
}