{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "author": "AlgorizeTech",
  "description": "Three image carousels in one file — a plain slider, a full-bleed hero with autoplay, and a thumbnail-navigated gallery. Reach for it for product shots, galleries and landing-page heroes.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/carousel.tsx",
      "content": "\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { ArrowLeft, ArrowRight, Hand, Pause, Play } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * An image source.\n *\n * A plain URL string in any React app. Next.js users importing a static asset\n * get a `StaticImageData` object instead, which `next/image` accepts — hence the\n * structural second branch rather than a `next/image` type import, which would\n * make this file Next-only.\n */\ntype StaticImageLike = { src: string; height?: number; width?: number; blurDataURL?: string };\ntype ImgSrc = string | StaticImageLike;\n\ntype ImageCarouselProps = {\n  images?: ImgSrc[];\n  rootClassName?: string;\n  slideClassName?: string;\n  arrowClassName?: string;\n  dotClassName?: string;\n  activeDotClassName?: string;\n  prevIcon?: React.ReactNode;\n  nextIcon?: React.ReactNode;\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  transitionDuration?: number;\n  transitionEasing?: string;\n  imageWidth?: number;\n  imageHeight?: number;\n  showArrows?: boolean;\n  showDots?: boolean;\n  /**\n   * The element used for images. Defaults to a plain `img`, so this component\n   * works in any React app with no adaptation. In Next.js, pass `next/image`\n   * to keep automatic optimisation: `imageComponent={Image}`.\n   */\n  imageComponent?: React.ElementType;\n};\n\nexport function ImageCarousel({\n  images = [],\n  rootClassName = \"relative h-60 w-full min-w-80 overflow-hidden rounded-lg md:h-[470px] md:rounded-xl lg:h-[540px]\",\n  slideClassName = \"h-60 min-w-full bg-black/20 object-cover sm:h-96 md:h-[540px]\",\n  arrowClassName = \"absolute top-1/2 z-50 flex h-7 w-7 items-center justify-center rounded-full bg-card/80 hover:bg-card shadow-md md:h-9 md:w-9 transition-all cursor-pointer\",\n  dotClassName = \"rounded-full bg-white duration-500 h-2\",\n  activeDotClassName = \"w-8\",\n  prevIcon,\n  nextIcon,\n  autoPlay = false,\n  autoPlayInterval = 3000,\n  transitionDuration = 500,\n  transitionEasing = \"ease-linear\",\n  imageWidth = 1200,\n  imageHeight = 540,\n  showArrows = true,\n  showDots = true,\n  imageComponent: Image = \"img\",\n}: ImageCarouselProps) {\n  const [currentSlider, setCurrentSlider] = useState<number>(0);\n\n  const defaultPrevIcon = (\n    <ArrowLeft className=\"h-4 w-4 text-foreground md:h-5 md:w-5\" />\n  );\n\n  const defaultNextIcon = (\n    <ArrowRight className=\"h-4 w-4 text-foreground md:h-5 md:w-5\" />\n  );\n\n  const prevSlider = () =>\n    setCurrentSlider((s) => (s === 0 ? images.length - 1 : s - 1));\n\n  const nextSlider = useCallback(() => {\n    setCurrentSlider((s) => (s === images.length - 1 ? 0 : s + 1));\n  }, [images.length]);\n\n  useEffect(() => {\n    if (autoPlay && images.length > 1) {\n      const interval = setInterval(nextSlider, autoPlayInterval);\n      return () => clearInterval(interval);\n    }\n  }, [autoPlay, autoPlayInterval, nextSlider, images.length]);\n\n  if (!images.length) return null;\n\n  return (\n    <div className={rootClassName}>\n      {showArrows && (\n        <button onClick={prevSlider} className={`left-3 ${arrowClassName}`}>\n          {prevIcon ?? defaultPrevIcon}\n        </button>\n      )}\n      {showArrows && (\n        <button onClick={nextSlider} className={`right-3 ${arrowClassName}`}>\n          {nextIcon ?? defaultNextIcon}\n        </button>\n      )}\n      {showDots && (\n        <div className=\"absolute bottom-4 z-50 flex w-full items-center justify-center gap-1 rounded-full\">\n          {images.map((_, idx) => (\n            <button\n              key={idx}\n              onClick={() => setCurrentSlider(idx)}\n              className={`${dotClassName} ${currentSlider === idx ? activeDotClassName : \"w-2\"}`}\n              aria-label={`Go to slide ${idx + 1}`}\n            />\n          ))}\n        </div>\n      )}\n      <div\n        className=\"relative flex transform-gpu\"\n        style={{\n          transform: `translateX(-${currentSlider * 100}%)`,\n          transitionDuration: `${transitionDuration}ms`,\n          transitionTimingFunction: transitionEasing,\n        }}\n      >\n        {images.map((img, idx) => (\n          <div key={`${String(img)}_${idx}`} className=\"min-w-full\">\n            <Image\n              width={imageWidth}\n              height={imageHeight}\n              src={img}\n              className={slideClassName}\n              alt={`Slider - ${idx + 1}`}\n            />\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport type HeroSlide = {\n  img: ImgSrc;\n  title?: string;\n  des?: string;\n};\n\nexport type HeroCarouselProps = {\n  slides?: HeroSlide[];\n  className?: string;\n  rootClassName?: string;\n  textContainerClassName?: string;\n  titleClassName?: string;\n  descriptionClassName?: string;\n  slideContainerClassName?: string;\n  slideClassName?: string;\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  transitionDuration?: number;\n  transitionEasing?: string;\n  imageWidth?: number;\n  imageHeight?: number;\n  /**\n   * The element used for images. Defaults to a plain `img`, so this component\n   * works in any React app with no adaptation. In Next.js, pass `next/image`\n   * to keep automatic optimisation: `imageComponent={Image}`.\n   */\n  imageComponent?: React.ElementType;\n};\n\nexport function HeroCarousel({\n  slides = [],\n  rootClassName = \"relative z-50 flex h-60 max-lg:h-[360px] w-full transform flex-col items-center justify-center gap-5 overflow-hidden rounded-lg bg-cover duration-1000 ease-linear before:absolute before:inset-0 before:bg-black/50 sm:h-96 md:h-[540px] md:rounded-xl lg:gap-10 xl:flex-row\",\n  textContainerClassName = \"absolute max-lg:top-12 max-lg:pl-[15px] left-0 w-full lg:w-1/2 rounded-lg px-4 text-white drop-shadow-lg z-40\",\n  titleClassName = \"mb-3 text-xl font-semibold sm:text-2xl lg:text-3xl\",\n  descriptionClassName = \"text-xs sm:text-sm md:text-base lg:text-lg max-lg:line-clamp-2\",\n  slideContainerClassName = \"absolute -right-5 max-lg:translate-y-[80px] z-50 ml-auto w-1/2 overflow-hidden px-4 py-10 lg:-right-16\",\n  slideClassName = \"h-[180px] min-w-[90px] sm:h-[200px] lg:h-[320px] lg:min-w-[184px] z-50 rounded-lg duration-300\",\n  autoPlay = false,\n  autoPlayInterval = 3000,\n  transitionDuration = 1000,\n  transitionEasing = \"ease-linear\",\n  imageWidth = 1200,\n  imageHeight = 540,\n  imageComponent: Image = \"img\",\n}: HeroCarouselProps) {\n  const [currentSlider, setCurrentSlider] = useState<number>(0);\n  const [animatingKey, setAnimatingKey] = useState<number>(0);\n\n  const nextSlider = useCallback(() => {\n    setCurrentSlider((s) => s + 1);\n    setAnimatingKey((k) => k + 1);\n  }, []);\n\n  useEffect(() => {\n    if (autoPlay && slides.length > 1) {\n      const interval = setInterval(nextSlider, autoPlayInterval);\n      return () => clearInterval(interval);\n    }\n  }, [autoPlay, autoPlayInterval, nextSlider, slides.length, currentSlider]);\n\n  const [slideWidth, setSlideWidth] = useState<number>(200);\n  const cardRef = React.useRef<HTMLDivElement>(null);\n\n  // Measures the first preview card so the track can be translated by an exact\n  // pixel amount. Kept above the `slides.length` guard: hooks must run in the\n  // same order on every render, including the one where `slides` is empty.\n  useEffect(() => {\n    const measure = () => {\n      if (cardRef.current) {\n        // 16px is the gap-4 distance between cards\n        setSlideWidth(cardRef.current.offsetWidth + 16);\n      }\n    };\n    measure();\n    window.addEventListener(\"resize\", measure);\n    return () => window.removeEventListener(\"resize\", measure);\n  }, []);\n\n  if (!slides.length) return null;\n\n  // Modulo math that supports negative numbers correctly\n  const positiveModulo = (n: number, m: number) => ((n % m) + m) % m;\n\n  const activeIndex = positiveModulo(currentSlider, slides.length);\n  const currentSlide = slides[activeIndex];\n\n  // Center offset base so negative indexing shifts track seamlessly in both directions\n  const baseOffsetIndex = 100 * slides.length;\n  // Exact offset so the 1st preview card aligns at the left edge of container (showing 2 full cards + 3rd cut off on right)\n  const virtualSliderIndex = baseOffsetIndex + currentSlider;\n\n  // Large infinite repeated slides array centered around base offset\n  const infiniteSlides = Array.from({ length: 200 }, () => slides).flat();\n\n  return (\n    <div\n      className={rootClassName}\n      style={{\n        backgroundImage: `url(${String(currentSlide.img)})`,\n        transitionDuration: `${transitionDuration}ms`,\n        transitionTimingFunction: transitionEasing,\n      }}\n    >\n      <div className={textContainerClassName}>\n        <div key={animatingKey} className=\"animate-in fade-in slide-in-from-bottom-5 duration-700 fill-mode-forwards\">\n          <h1 className={titleClassName}>{currentSlide.title}</h1>\n          <p className={descriptionClassName}>{currentSlide.des}</p>\n        </div>\n      </div>\n      <div className={slideContainerClassName}>\n        <div\n          className=\"flex items-center gap-4 duration-500 ease-in-out transition-transform\"\n          style={{\n            transform: `translateX(-${virtualSliderIndex * slideWidth}px)`,\n          }}\n        >\n          {infiniteSlides.map((slide, inx) => {\n            const slideIndex = positiveModulo(inx, slides.length);\n            return (\n              <div\n                key={inx}\n                ref={inx === 0 ? cardRef : undefined}\n                onClick={() => {\n                  setCurrentSlider(inx - baseOffsetIndex);\n                  setAnimatingKey((k) => k + 1);\n                }}\n                className={`${slideClassName} cursor-pointer transition-transform hover:scale-105 ${\n                  slideIndex === activeIndex ? \"ring-2 ring-white/80 shadow-lg scale-102\" : \"opacity-80 hover:opacity-100\"\n                }`}\n              >\n                <Image\n                  width={imageWidth}\n                  height={imageHeight}\n                  src={slide.img}\n                  alt={slide.title ?? `slide-${inx}`}\n                  className=\"h-full w-full object-cover rounded-lg\"\n                />\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport type CustomItem = {\n  img: ImgSrc;\n  title?: string;\n  des?: string;\n};\n\nexport type CustomCarouselProps = {\n  items?: CustomItem[];\n  className?: string;\n  rootClassName?: string;\n  textClassName?: string;\n  titleClassName?: string;\n  descClassName?: string;\n  thumbClassName?: string;\n  activeThumbClassName?: string;\n  autoPlay?: boolean;\n  interval?: number;\n  imageButtonPosition?: \"bottom\" | \"left\" | \"right\";\n  /**\n   * The element used for images. Defaults to a plain `img`, so this component\n   * works in any React app with no adaptation. In Next.js, pass `next/image`\n   * to keep automatic optimisation: `imageComponent={Image}`.\n   */\n  imageComponent?: React.ElementType;\n};\n\nexport function CustomCarousel({\n  items = [],\n  rootClassName = \"flex h-60 w-full flex-col items-center justify-center gap-5 overflow-hidden rounded-lg bg-cover bg-center sm:h-96 md:h-[540px] md:rounded-xl lg:gap-10\",\n  textClassName = \"px-5 text-center text-white drop-shadow-lg duration-300 ease-linear\",\n  titleClassName = \"mb-3 text-xl font-semibold lg:text-3xl\",\n  descClassName = \"text-sm md:text-base lg:text-lg\",\n  thumbClassName = \"h-6 w-10 shrink-0 overflow-hidden bg-black/20 sm:h-8 md:h-12 md:w-20 cursor-pointer rounded-md md:rounded-lg\",\n  activeThumbClassName = \"outline-2 outline-offset-2 outline-primary\",\n  autoPlay = true,\n  interval = 5000,\n  imageButtonPosition = \"bottom\",\n  imageComponent: Image = \"img\",\n}: CustomCarouselProps) {\n  const [current, setCurrent] = useState<number>(0);\n\n  useEffect(() => {\n    if (!autoPlay || items.length <= 1) return;\n    const id = setInterval(() => {\n      setCurrent((prev) => (prev === items.length - 1 ? 0 : prev + 1));\n    }, interval);\n    return () => clearInterval(id);\n  }, [autoPlay, interval, items.length]);\n\n  if (!items.length) return null;\n\n  const CarouselContent = (\n    <div\n      className={`${rootClassName} relative min-w-0 duration-1000 ease-linear before:absolute before:inset-0 before:bg-black/50`}\n      style={{ backgroundImage: `url(${String(items[current].img)})` }}\n    >\n      <div className={textClassName}>\n        <h1 className={titleClassName}>{items[current].title}</h1>\n        <p className={descClassName}>{items[current].des}</p>\n      </div>\n    </div>\n  );\n\n  const Thumbnails = (\n    <div\n      className={`${imageButtonPosition === \"bottom\" ? \"w-full flex-row flex-wrap justify-center\" : \"shrink-0 flex-col justify-center\"} flex items-center gap-3 p-2`}\n    >\n      {items.map((item, index) => (\n        <button\n          key={index}\n          onClick={() => setCurrent(index)}\n          className={`${thumbClassName} ${current === index ? activeThumbClassName : \"\"}`}\n          aria-label={`Select slide ${index + 1}`}\n        >\n          <Image\n            src={item.img}\n            width={1200}\n            height={540}\n            className=\"h-full w-full object-cover\"\n            alt={item.title ?? `thumb-${index}`}\n          />\n        </button>\n      ))}\n    </div>\n  );\n\n  if (imageButtonPosition === \"left\")\n    return (\n      <div className=\"flex w-full items-center\">\n        {Thumbnails}\n        {CarouselContent}\n      </div>\n    );\n  if (imageButtonPosition === \"right\")\n    return (\n      <div className=\"flex w-full items-center\">\n        {CarouselContent}\n        {Thumbnails}\n      </div>\n    );\n  return (\n    <div className=\"w-full\">\n      {CarouselContent}\n      {Thumbnails}\n    </div>\n  );\n}\n\nexport type FanCarouselItem = {\n  img: ImgSrc;\n  title: string;\n  price?: string;\n  rating?: number | string;\n  category?: string;\n  des?: string;\n};\n\nexport type RadialCarouselItem = FanCarouselItem;\n\nexport type FanCarouselProps = {\n  items?: FanCarouselItem[];\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  stepAngle?: number;\n  showArrows?: boolean;\n  showDots?: boolean;\n  className?: string;\n  /**\n   * The element used for images. Defaults to a plain `img`, so this component\n   * works in any React app with no adaptation. In Next.js, pass `next/image`\n   * to keep automatic optimisation: `imageComponent={Image}`.\n   */\n  imageComponent?: React.ElementType;\n};\n\nexport type RadialCarouselProps = FanCarouselProps;\n\nconst defaultRadialItems: FanCarouselItem[] = [\n  {\n    title: \"Misty Pine Forest\",\n    category: \"Nature\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n  },\n  {\n    title: \"Lush River Valley\",\n    category: \"Wilderness\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n  },\n  {\n    title: \"Autumn Woodland Path\",\n    category: \"Landscape\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n  },\n  {\n    title: \"Cascading Waterfalls\",\n    category: \"Sanctuary\",\n    rating: \"4.7\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\",\n  },\n  {\n    title: \"Emerald Mountain Peak\",\n    category: \"Expedition\",\n    rating: \"5.0\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n  },\n  {\n    title: \"Serene Alpine Lake\",\n    category: \"Reflections\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n  },\n  {\n    title: \"Sunset Coastal Shore\",\n    category: \"Ocean\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1507525428034-b723cf961d3e-1790146212208.jpg\",\n  },\n  {\n    title: \"Northern Wilderness\",\n    category: \"Aurora\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1517411032315-54ef2cb783bb-1790146212209.jpg\",\n  },\n  {\n    title: \"Golden Desert Dunes\",\n    category: \"Landscape\",\n    rating: \"4.6\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1509316975850-ff9c5deb0cd9-1790146212209.jpg\",\n  },\n  {\n    title: \"Grand Canyon River\",\n    category: \"Sanctuary\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\",\n  },\n  {\n    title: \"Alpine Cloud Ridge\",\n    category: \"Expedition\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n  },\n  {\n    title: \"Tropical Lagoon Bay\",\n    category: \"Paradise\",\n    rating: \"5.0\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1506905925346-21bda4d32df4-1790146212208.jpg\",\n  },\n  {\n    title: \"Mist Forest Ridge\",\n    category: \"Wilderness\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790148863/photo-1542273917363-3b1817f69a2d-1790148865110.jpg\",\n  },\n  {\n    title: \"Glacial Ice Valley\",\n    category: \"Arctic\",\n    rating: \"5.0\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146209/photo-1483728642387-6c3bdd6c93e5-1790146212206.jpg\",\n  },\n  {\n    title: \"Sunlit Birch Grove\",\n    category: \"Forest\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145142/photo-1500530855697-b586d89ba3ee-1790145143493.jpg\",\n  },\n  {\n    title: \"Starlight Mountain Peak\",\n    category: \"Cosmos\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146211/photo-1519681393784-d120267933ba-1790146212210.jpg\",\n  },\n  {\n    title: \"Volcanic Obsidian Coast\",\n    category: \"Geothermal\",\n    rating: \"4.7\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146209/photo-1500382017468-9049fed747ef-1790146212206.jpg\",\n  },\n  {\n    title: \"Deep Canyon Oasis\",\n    category: \"Sanctuary\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n  },\n];\n\nexport function RadialCarousel({\n  items = defaultRadialItems,\n  autoPlay = false,\n  autoPlayInterval = 3500,\n  stepAngle,\n  showArrows = false,\n  showDots = false,\n  className = \"\",\n  imageComponent: Image = \"img\",\n}: RadialCarouselProps) {\n  const [current, setCurrent] = useState<number>(0);\n  const [isDragging, setIsDragging] = useState(false);\n  const [dragStartX, setDragStartX] = useState(0);\n  const [dragOffset, setDragOffset] = useState(0);\n  // `autoPlay` sets the starting state; the hub button toggles it from there.\n  // A new `autoPlay` value resets it, adjusted while rendering rather than in\n  // an effect.\n  const [isPlaying, setIsPlaying] = useState(autoPlay);\n  const [prevAutoPlay, setPrevAutoPlay] = useState(autoPlay);\n  if (autoPlay !== prevAutoPlay) {\n    setPrevAutoPlay(autoPlay);\n    setIsPlaying(autoPlay);\n  }\n\n  const displayItems = React.useMemo(() => {\n    if (!items || !items.length) return [];\n    if (items.length >= 10) return items;\n    const repeats = Math.ceil(12 / items.length);\n    return Array.from({ length: repeats }, () => items).flat();\n  }, [items]);\n\n  const total = displayItems.length;\n  const angleStep = stepAngle ?? (total > 0 ? 360 / total : 20);\n\n  const prevSlide = useCallback(() => {\n    setCurrent((prev) => prev - 1);\n  }, []);\n\n  const nextSlide = useCallback(() => {\n    setCurrent((prev) => prev + 1);\n  }, []);\n\n  useEffect(() => {\n    if (!isPlaying || total <= 1 || isDragging) return;\n    const interval = setInterval(nextSlide, autoPlayInterval);\n    return () => clearInterval(interval);\n  }, [isPlaying, autoPlayInterval, nextSlide, total, current, isDragging]);\n\n  if (!total) return null;\n\n  const dragAngle = dragOffset / 7; // Sensitivity factor: 7px = 1 degree\n\n  const handleMouseDown = (e: React.MouseEvent | React.TouchEvent, isCardCenter: boolean) => {\n    if (!isCardCenter) return;\n    setIsDragging(true);\n    const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\n    setDragStartX(clientX);\n    setDragOffset(0);\n  };\n\n  const handleMouseMove = (e: React.MouseEvent | React.TouchEvent) => {\n    if (!isDragging) return;\n    const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\n    setDragOffset(clientX - dragStartX);\n  };\n\n  const handleMouseUp = () => {\n    if (!isDragging) return;\n    setIsDragging(false);\n    const stepShift = Math.round(dragAngle / angleStep);\n    if (stepShift !== 0) {\n      setCurrent((prev) => prev - stepShift);\n    }\n    setDragOffset(0);\n  };\n\n  const positiveModulo = (n: number, m: number) => ((n % m) + m) % m;\n\n  const wheelRotation = -current * angleStep + dragAngle;\n  const activeIndex = positiveModulo(current, items.length);\n\n  return (\n    <div\n      onMouseMove={handleMouseMove}\n      onMouseUp={handleMouseUp}\n      onMouseLeave={handleMouseUp}\n      onTouchMove={handleMouseMove}\n      onTouchEnd={handleMouseUp}\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label=\"Image carousel\"\n      className={`relative w-full max-w-full overflow-hidden py-4 flex flex-col items-center justify-center select-none ${className}`}\n    >\n      <p className=\"sr-only\" aria-live={isPlaying ? \"off\" : \"polite\"}>\n        {items[activeIndex]?.title}\n      </p>\n\n      <div className=\"relative w-full max-w-3xl h-[360px] sm:h-[540px] md:h-[660px]\">\n        {/* The axle: a zero-size point every card pivots around. It sits below\n            the stage's middle because the top card is lifted and scaled up\n            while the bottom cards shrink, so the wheel is taller above it. */}\n        <div className=\"absolute left-1/2 top-[calc(50%+25px)] sm:top-[calc(50%+37px)] md:top-[calc(50%+45px)] w-0 h-0\">\n          {displayItems.map((item, index) => {\n            const cardBaseAngle = index * angleStep;\n            const totalAngle = cardBaseAngle + wheelRotation;\n\n            let normAngle = positiveModulo(totalAngle, 360);\n            if (normAngle > 180) normAngle -= 360;\n\n            const distRatio = Math.abs(normAngle) / 180;\n            const isCenter = Math.abs(normAngle) < 1.5;\n\n            const zIndex = Math.round(60 - distRatio * 40);\n            const scale = isCenter ? 1.05 : Math.max(0.8, 1 - distRatio * 0.2);\n            const translateY = isCenter ? -10 : 0;\n            // Cards stay opaque and wash toward the page colour with distance,\n            // so overlapping cards never show through each other.\n            const fade = isCenter ? 0 : Math.min(0.62, 0.12 + distRatio * 0.55);\n            const saturation = isCenter ? 1 : 1 - distRatio * 0.45;\n\n            return (\n              <div\n                key={index}\n                onMouseDown={(e) => handleMouseDown(e, isCenter)}\n                onTouchStart={(e) => handleMouseDown(e, isCenter)}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  if (!isCenter) return; // Clicks on non-active background cards do NOTHING!\n                  if (Math.abs(dragOffset) > 5) return;\n                  nextSlide();\n                }}\n                style={{\n                  top: 0,\n                  left: 0,\n                  // Pivoting 18% of the card height below its bottom edge\n                  // leaves an open ring at the centre for the hub.\n                  transformOrigin: \"50% 118%\",\n                  transform: `translate(-50%, -118%) rotate(${totalAngle}deg) translateY(${translateY}px) scale(${scale})`,\n                  zIndex,\n                  filter: `saturate(${saturation})`,\n                }}\n                className={`absolute w-[104px] sm:w-[160px] md:w-[200px] h-[140px] sm:h-[216px] md:h-[272px] rounded-[14px] sm:rounded-[18px] md:rounded-[22px] overflow-hidden cursor-grab active:cursor-grabbing ${\n                  isDragging\n                    ? \"transition-none\"\n                    : \"transition-[transform,filter,box-shadow] duration-[850ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none\"\n                } ${\n                  isCenter\n                    ? \"ring-2 ring-primary ring-offset-2 ring-offset-background shadow-[0_28px_48px_-20px_rgb(0_0_0/0.55)]\"\n                    : \"ring-1 ring-black/10 dark:ring-white/10 shadow-[0_6px_18px_-10px_rgb(0_0_0/0.35)]\"\n                }`}\n              >\n                <div className=\"relative w-full h-full\">\n                  <Image\n                    src={item.img}\n                    alt={item.title}\n                    width={600}\n                    height={800}\n                    className=\"w-full h-full object-cover pointer-events-none\"\n                  />\n\n                  <div\n                    aria-hidden\n                    style={{ opacity: fade }}\n                    className=\"absolute inset-0 bg-background transition-opacity duration-[850ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none\"\n                  />\n\n                  <div\n                    aria-hidden={!isCenter}\n                    className={`absolute inset-0 flex flex-col justify-end p-2.5 sm:p-4 md:p-5 text-white bg-gradient-to-t from-black/80 via-black/25 via-45% to-transparent transition-opacity duration-500 motion-reduce:transition-none ${\n                      isCenter ? \"opacity-100\" : \"opacity-0\"\n                    }`}\n                  >\n                    <h3 className=\"font-serif font-medium text-[13px] sm:text-lg md:text-[22px] leading-[1.1] tracking-[-0.01em] text-balance line-clamp-2 [text-shadow:0_1px_12px_rgb(0_0_0/0.35)]\">\n                      {item.title}\n                    </h3>\n\n                    <div className=\"flex items-center justify-between gap-2 mt-1.5 sm:mt-2 text-[10px] sm:text-xs md:text-[13px] text-white/75\">\n                      {item.price ? (\n                        <span className=\"font-medium text-white/90 tabular-nums\">{item.price}</span>\n                      ) : item.category ? (\n                        <span className=\"truncate\">{item.category}</span>\n                      ) : (\n                        <span />\n                      )}\n\n                      {item.rating && (\n                        <span className=\"flex items-center gap-1 shrink-0 font-medium text-white tabular-nums\">\n                          <span aria-hidden className=\"text-amber-300\">★</span>\n                          {item.rating}\n                        </span>\n                      )}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n\n          {/* Hub: centred on the axle (the wheel's exact centre); starts and\n              stops autoplay. */}\n          <button\n            type=\"button\"\n            onClick={() => setIsPlaying((p) => !p)}\n            onMouseDown={(e) => e.stopPropagation()}\n            onTouchStart={(e) => e.stopPropagation()}\n            aria-label={isPlaying ? \"Pause autoplay\" : \"Play autoplay\"}\n            className=\"absolute left-0 top-0 z-[70] -translate-x-1/2 -translate-y-1/2 size-11 sm:size-16 md:size-[76px] rounded-full bg-background text-foreground ring-1 ring-black/10 dark:ring-white/15 shadow-[0_10px_30px_-12px_rgb(0_0_0/0.45),inset_0_1px_0_rgb(255_255_255/0.6)] dark:shadow-[0_10px_30px_-12px_rgb(0_0_0/0.8),inset_0_1px_0_rgb(255_255_255/0.08)] grid place-items-center cursor-pointer transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\"\n          >\n            {isPlaying ? (\n              <Pause aria-hidden className=\"block size-4 sm:size-5 md:size-6\" fill=\"currentColor\" strokeWidth={0} />\n            ) : (\n              <Play aria-hidden className=\"block size-4 sm:size-5 md:size-6\" fill=\"currentColor\" strokeWidth={0} />\n            )}\n          </button>\n        </div>\n      </div>\n\n      {(showArrows || showDots) && (\n        <div className=\"flex items-center gap-4 mt-2 z-10\">\n          {showArrows && (\n            <button\n              onClick={prevSlide}\n              className=\"p-2.5 rounded-full bg-background border border-border text-foreground hover:bg-muted transition-all cursor-pointer shadow-md\"\n              aria-label=\"Previous card\"\n            >\n              <ArrowLeft className=\"w-4 h-4\" />\n            </button>\n          )}\n\n          {showDots && (\n            <div className=\"flex items-center gap-1.5 max-w-[200px] overflow-hidden flex-wrap justify-center\">\n              {items.map((_, idx) => (\n                <button\n                  key={idx}\n                  onClick={() => {\n                    const activeIdx = positiveModulo(current, items.length);\n                    let diff = idx - activeIdx;\n                    if (diff > Math.floor(items.length / 2)) diff -= items.length;\n                    if (diff < -Math.floor(items.length / 2)) diff += items.length;\n                    setCurrent((c) => c + diff);\n                  }}\n                  className={`h-2 rounded-full transition-all duration-300 ${\n                    positiveModulo(current, items.length) === idx\n                      ? \"w-6 bg-primary\"\n                      : \"w-2 bg-foreground/20\"\n                  }`}\n                  aria-label={`Go to slide ${idx + 1}`}\n                />\n              ))}\n            </div>\n          )}\n\n          {showArrows && (\n            <button\n              onClick={nextSlide}\n              className=\"p-2.5 rounded-full bg-background border border-border text-foreground hover:bg-muted transition-all cursor-pointer shadow-md\"\n              aria-label=\"Next card\"\n            >\n              <ArrowRight className=\"w-4 h-4\" />\n            </button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nconst defaultFanDeckItems: FanCarouselItem[] = [\n  {\n    title: \"FUTURE UI\",\n    price: \"$145.00\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790148864/photo-1618005182384-a83a8bd57fbe-1790148865111.jpg\",\n  },\n  {\n    title: \"NIKE AIR RETRO\",\n    price: \"$199.99\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790148863/photo-1542291026-7eec264c27ff-1790148865109.jpg\",\n  },\n  {\n    title: \"RAMEN BOWL\",\n    price: \"$18.50\",\n    rating: \"4.7\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790148864/photo-1569718212165-3a8278d5f624-1790148865111.jpg\",\n  },\n  {\n    title: \"SKATEBOARD PRO\",\n    price: \"$129.00\",\n    rating: \"4.6\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto/v1790224415/photo-1501785888041-af3ef285b470-1790146212207-1790224416318.jpg\",\n  },\n  {\n    title: \"CYBER CITY\",\n    price: \"$210.00\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1518709268805-4e9042af9f23-1790146212209.jpg\",\n  },\n  {\n    title: \"NEON SOUNDS\",\n    price: \"$85.00\",\n    rating: \"4.8\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1507525428034-b723cf961d3e-1790146212208.jpg\",\n  },\n  {\n    title: \"PEAK RETREAT\",\n    price: \"$399.00\",\n    rating: \"4.9\",\n    img: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n  },\n];\n\nexport function FanCarousel({\n  items = defaultFanDeckItems,\n  autoPlay = false,\n  autoPlayInterval = 3500,\n  stepAngle,\n  showArrows = false,\n  showDots = false,\n  className = \"\",\n  imageComponent: Image = \"img\",\n}: FanCarouselProps) {\n  const [current, setCurrent] = useState<number>(0);\n  const [isDragging, setIsDragging] = useState(false);\n  const [dragStartX, setDragStartX] = useState(0);\n  const [dragOffset, setDragOffset] = useState(0);\n\n  const displayItems = React.useMemo(() => {\n    if (!items || !items.length) return [];\n    if (items.length >= 10) return items;\n    const repeats = Math.ceil(12 / items.length);\n    return Array.from({ length: repeats }, () => items).flat();\n  }, [items]);\n\n  const total = displayItems.length;\n  const angleStep = stepAngle ?? (total > 0 ? 360 / total : 20);\n\n  const prevSlide = useCallback(() => {\n    setCurrent((prev) => prev - 1);\n  }, []);\n\n  const nextSlide = useCallback(() => {\n    setCurrent((prev) => prev + 1);\n  }, []);\n\n  useEffect(() => {\n    if (!autoPlay || total <= 1 || isDragging) return;\n    const interval = setInterval(nextSlide, autoPlayInterval);\n    return () => clearInterval(interval);\n  }, [autoPlay, autoPlayInterval, nextSlide, total, current, isDragging]);\n\n  if (!total) return null;\n\n  const dragAngle = dragOffset / 7; // Sensitivity factor: 7px = 1 degree\n\n  const handleMouseDown = (e: React.MouseEvent | React.TouchEvent, isCardCenter: boolean) => {\n    if (!isCardCenter) return;\n    setIsDragging(true);\n    const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\n    setDragStartX(clientX);\n    setDragOffset(0);\n  };\n\n  const handleMouseMove = (e: React.MouseEvent | React.TouchEvent) => {\n    if (!isDragging) return;\n    const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\n    setDragOffset(clientX - dragStartX);\n  };\n\n  const handleMouseUp = () => {\n    if (!isDragging) return;\n    setIsDragging(false);\n    const stepShift = Math.round(dragAngle / angleStep);\n    if (stepShift !== 0) {\n      setCurrent((prev) => prev - stepShift);\n    }\n    setDragOffset(0);\n  };\n\n  const positiveModulo = (n: number, m: number) => ((n % m) + m) % m;\n\n  const wheelRotation = -current * angleStep + dragAngle;\n\n  return (\n    <div\n      onMouseMove={handleMouseMove}\n      onMouseUp={handleMouseUp}\n      onMouseLeave={handleMouseUp}\n      onTouchMove={handleMouseMove}\n      onTouchEnd={handleMouseUp}\n      className={`relative w-full max-w-full overflow-hidden py-4 flex flex-col items-center justify-center select-none ${className}`}\n    >\n      {/* Hand Gesture Hint Badge */}\n      <div className=\"absolute top-2 right-4 z-20 flex items-center gap-1.5 px-3 py-1 rounded-full bg-black/70 backdrop-blur-md text-[11px] font-medium text-white/90 border border-white/10 pointer-events-none shadow-lg\">\n        <Hand className=\"w-3.5 h-3.5 text-amber-400 animate-pulse\" />\n        <span>Drag active card</span>\n      </div>\n\n      {/* 360 Degree Fan Wheel Container */}\n      <div className=\"relative w-full max-w-3xl h-[520px] sm:h-[600px] md:h-[680px] flex items-center justify-center\">\n        {displayItems.map((item, index) => {\n          const cardBaseAngle = index * angleStep;\n          const totalAngle = cardBaseAngle + wheelRotation;\n\n          let normAngle = positiveModulo(totalAngle, 360);\n          if (normAngle > 180) normAngle -= 360;\n\n          const distRatio = Math.abs(normAngle) / 180;\n          const isCenter = Math.abs(normAngle) < 1.5;\n\n          const zIndex = Math.round(60 - distRatio * 40);\n          const scale = isCenter ? 1.05 : Math.max(0.72, 1 - distRatio * 0.28);\n          const translateY = isCenter ? -10 : 0;\n          const opacity = isCenter ? 1 : Math.max(0.7, 1 - distRatio * 0.32);\n          const brightness = isCenter ? 1 : Math.max(0.55, 1 - distRatio * 0.42);\n\n          return (\n            <div\n              key={index}\n              onMouseDown={(e) => handleMouseDown(e, isCenter)}\n              onTouchStart={(e) => handleMouseDown(e, isCenter)}\n              onClick={(e) => {\n                e.stopPropagation();\n                if (!isCenter) return; // Clicks on non-active background cards do NOTHING!\n                if (Math.abs(dragOffset) > 5) return;\n                nextSlide();\n              }}\n              style={{\n                top: \"50%\",\n                left: \"50%\",\n                transformOrigin: \"50% 105%\",\n                transform: `translate(-50%, -100%) rotate(${totalAngle}deg) translateY(${translateY}px) scale(${scale})`,\n                zIndex,\n                opacity,\n                filter: `brightness(${brightness})`,\n                transition: isDragging\n                  ? \"none\"\n                  : \"transform 0.85s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.85s cubic-bezier(0.16, 1, 0.3, 1), filter 0.85s cubic-bezier(0.16, 1, 0.3, 1)\",\n              }}\n              className={`absolute w-[110px] sm:w-[170px] md:w-[220px] h-[150px] sm:h-[230px] md:h-[300px] rounded-[14px] sm:rounded-[20px] md:rounded-[24px] overflow-hidden border-2 border-black/80 dark:border-black shadow-2xl cursor-grab active:cursor-grabbing ${\n                isCenter\n                  ? \"ring-2 ring-primary shadow-black/60 brightness-105\"\n                  : \"hover:brightness-95\"\n              }`}\n            >\n              <div className=\"relative w-full h-full\">\n                <Image\n                  src={item.img}\n                  alt={item.title}\n                  width={600}\n                  height={800}\n                  className=\"w-full h-full object-cover pointer-events-none\"\n                />\n\n                <div className=\"absolute inset-0 bg-gradient-to-t from-black/95 via-black/40 to-transparent\" />\n\n                <div className=\"absolute inset-x-0 bottom-0 p-2 sm:p-3.5 md:p-5 flex flex-col justify-end text-white\">\n                  <h3 className=\"text-[11px] sm:text-base md:text-lg font-extrabold tracking-wide uppercase drop-shadow-md line-clamp-1\">\n                    {item.title}\n                  </h3>\n\n                  <div className=\"flex items-center justify-between mt-0.5 text-[9px] sm:text-xs md:text-sm\">\n                    {item.price ? (\n                      <span className=\"font-semibold text-zinc-200\">{item.price}</span>\n                    ) : item.category ? (\n                      <span className=\"text-[8px] sm:text-[10px] md:text-xs uppercase font-semibold text-zinc-300 px-1.5 py-0.5 rounded-full bg-white/10 backdrop-blur-sm truncate max-w-[60px] sm:max-w-none\">\n                        {item.category}\n                      </span>\n                    ) : null}\n\n                    {item.rating && (\n                      <div className=\"flex items-center gap-0.5 bg-black/50 backdrop-blur-md px-1 sm:px-1.5 py-0.5 rounded-full text-[9px] sm:text-xs font-semibold text-amber-400 border border-white/10\">\n                        <span>★</span>\n                        <span className=\"text-white\">{item.rating}</span>\n                      </div>\n                    )}\n                  </div>\n                </div>\n              </div>\n            </div>\n          );\n        })}\n\n        {/* Center Pivot Hub Pin / Dot Button matching uploaded screenshot */}\n        <div\n          onClick={nextSlide}\n          title=\"Rotate wheel\"\n          className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[70] w-9 h-9 sm:w-11 sm:h-11 rounded-full bg-zinc-950/90 border-2 border-zinc-700 shadow-2xl flex items-center justify-center cursor-pointer hover:scale-110 active:scale-95 transition-transform\"\n        >\n          <div className=\"w-3 h-3 sm:w-4 sm:h-4 rounded-full bg-zinc-400/90 shadow-inner\" />\n        </div>\n      </div>\n\n      {showArrows && (\n        <div className=\"mt-4 flex items-center gap-3\">\n          <button\n            type=\"button\"\n            onClick={prevSlide}\n            aria-label=\"Previous slide\"\n            className=\"p-2.5 rounded-full bg-zinc-950/90 border border-zinc-700 text-white transition-transform hover:scale-110 active:scale-95\"\n          >\n            <ArrowLeft className=\"w-4 h-4\" />\n          </button>\n          <button\n            type=\"button\"\n            onClick={nextSlide}\n            aria-label=\"Next slide\"\n            className=\"p-2.5 rounded-full bg-zinc-950/90 border border-zinc-700 text-white transition-transform hover:scale-110 active:scale-95\"\n          >\n            <ArrowRight className=\"w-4 h-4\" />\n          </button>\n        </div>\n      )}\n\n      {showDots && (\n        <div className=\"mt-3 flex flex-wrap items-center justify-center gap-2\">\n          {displayItems.map((_, index) => (\n            <button\n              key={index}\n              type=\"button\"\n              onClick={() => setCurrent(index)}\n              aria-label={`Go to slide ${index + 1}`}\n              aria-current={positiveModulo(current, total) === index}\n              className={cn(\n                \"h-2 rounded-full transition-all\",\n                positiveModulo(current, total) === index\n                  ? \"w-6 bg-primary\"\n                  : \"w-2 bg-foreground/20\",\n              )}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\n/* ============================================================================\n   FLUID GLASS SLIDER COMPONENT (Liquid Distortion Slider)\n   ============================================================================ */\n\n// ==========================================\n// MOTION — Web Animations and CSS, no library\n// ==========================================\n// GSAP's power4.inOut (sampled, max error 0.15%), power2.out and power1.out.\nconst POWER4_IN_OUT =\n  \"linear(0, .0005 12.5%, .0037 18.75%, .008 21.875%, .0156 25%, .0282 28.125%, .0477 31.25%, .0768 34.375%, .0959 35.938%, .1187 37.5%, .1455 39.063%, .177 40.625%, .2138 42.188%, .2565 43.75%, .3056 45.313%, .3621 46.875%, .4266 48.438%, .5 50%, .5734 51.563%, .6379 53.125%, .6944 54.688%, .7435 56.25%, .7862 57.813%, .823 59.375%, .8545 60.938%, .8813 62.5%, .9041 64.063%, .9232 65.625%, .9523 68.75%, .9718 71.875%, .9844 75%, .992 78.125%, .9963 81.25%, .9995 87.5%, 1)\";\nconst POWER2_OUT = \"cubic-bezier(.3333, 1, .6667, 1)\";\nconst POWER1_OUT = \"cubic-bezier(.3333, .6667, .6667, 1)\";\n\nconst tweens = new WeakMap<HTMLElement, Animation>();\n\n/**\n * Tween `el` to `to` from wherever it is right now (mid-flight included), so a\n * new target takes over a running tween with the full duration. The end values\n * are written inline, so nothing is held by a fill.\n */\nfunction tweenTo(el: HTMLElement, to: Record<string, string>, duration: number, easing: string) {\n  const computed = getComputedStyle(el);\n  const from: Record<string, string> = {};\n  for (const key of Object.keys(to)) from[key] = computed.getPropertyValue(key);\n  tweens.get(el)?.cancel();\n  for (const [key, value] of Object.entries(to)) el.style.setProperty(key, value);\n  if (typeof el.animate !== \"function\") return null;\n  const animation = el.animate([from, to], { duration: duration * 1000, easing });\n  tweens.set(el, animation);\n  return animation;\n}\n\nexport type FluidGlassSlide = {\n  id: number;\n  image: string;\n  title: string;\n  subtitle: string;\n};\n\nconst DEFAULT_GLASS_SLIDES: FluidGlassSlide[] = [\n  {\n    id: 1,\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_2574,c_limit/v1790146210/photo-1506905925346-21bda4d32df4-1790146212208.jpg\",\n    title: \"Discover pristine tropical beaches and crystal-clear waters\",\n    subtitle: \"TROPICAL PARADISE\",\n  },\n  {\n    id: 2,\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_2574,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n    title: \"Escape to the serene mountain heights and wilderness trails\",\n    subtitle: \"MOUNTAIN ADVENTURES\",\n  },\n  {\n    id: 3,\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_2574,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n    title: \"Surround yourself with majestic peaks and alpine lakes\",\n    subtitle: \"ALPINE RETREAT\",\n  },\n];\n\nexport type FluidGlassSliderProps = {\n  slides?: FluidGlassSlide[];\n  className?: string;\n};\n\nexport function FluidGlassSlider({ slides, className }: FluidGlassSliderProps) {\n  const slidesData = slides && slides.length > 0 ? slides : DEFAULT_GLASS_SLIDES;\n  const [currentIndex, setCurrentIndex] = useState<number>(0);\n\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const sliderRef = useRef<HTMLDivElement | null>(null);\n  const progressRef = useRef<HTMLDivElement | null>(null);\n  const cursorRef = useRef<HTMLDivElement | null>(null);\n\n  const isFirstSlide = currentIndex === 0;\n  const isLastSlide = currentIndex === slidesData.length - 1;\n\n  useEffect(() => {\n    if (!sliderRef.current) return;\n\n    // Slide animation\n    const slide = tweenTo(\n      sliderRef.current,\n      { translate: `-${currentIndex * (100 / slidesData.length)}% 0px` },\n      1.2,\n      POWER4_IN_OUT,\n    );\n\n    if (!progressRef.current || typeof progressRef.current.animate !== \"function\") return;\n\n    // Auto-play progress bar (Infinite Loop)\n    const progress = progressRef.current.animate(\n      [{ width: \"0%\" }, { width: \"100%\" }],\n      { duration: 5000, easing: \"linear\", fill: \"forwards\" },\n    );\n    progress.onfinish = () => {\n      // Auto-play continues infinitely\n      setCurrentIndex((prev) =>\n        prev < slidesData.length - 1 ? prev + 1 : 0,\n      );\n    };\n\n    // A hidden tab holds both where they are, as it always has: the timer\n    // must not run on and flip slides nobody is watching.\n    const onVisibility = () => {\n      for (const animation of [slide, progress]) {\n        if (!animation || animation.playState === \"finished\") continue;\n        if (document.hidden) animation.pause();\n        else animation.play();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    onVisibility();\n\n    return () => {\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      progress.onfinish = null;\n      progress.cancel();\n    };\n  }, [currentIndex, slidesData.length]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const cursor = cursorRef.current;\n    if (!container || !cursor) return;\n\n    // Follows the pointer through a 0.1s transition on `translate` (see the\n    // cursor's style).\n    const onMouseMove = (e: MouseEvent) => {\n      if (!cursor) return;\n      cursor.style.translate = `${(e.clientX || 0) + 15}px ${(e.clientY || 0) + 15}px`;\n    };\n\n    container.addEventListener(\"mousemove\", onMouseMove);\n    return () => container.removeEventListener(\"mousemove\", onMouseMove);\n  }, []);\n\n  const nextSlide = () => {\n    if (!isLastSlide) setCurrentIndex((prev) => prev + 1);\n  };\n\n  const prevSlide = () => {\n    if (!isFirstSlide) setCurrentIndex((prev) => prev - 1);\n  };\n\n  const showCursor = () => {\n    if (!cursorRef.current) return;\n    tweenTo(cursorRef.current, { opacity: \"1\", scale: \"1\" }, 0.2, POWER1_OUT);\n  };\n  const hideCursor = () => {\n    if (!cursorRef.current) return;\n    tweenTo(cursorRef.current, { opacity: \"0\", scale: \"0.8\" }, 0.2, POWER1_OUT);\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      onMouseEnter={showCursor}\n      onMouseLeave={hideCursor}\n      className={cn(\n        \"relative w-full h-[500px] md:h-[650px] overflow-hidden bg-[#111] text-white font-sans rounded-2xl shadow-xl\",\n        className,\n      )}\n    >\n      <div\n        ref={cursorRef}\n        className=\"fixed top-0 left-0 px-3 py-1.5 bg-white/20 backdrop-blur-md border border-white/30 text-white text-[10px] font-semibold tracking-widest uppercase flex items-center justify-center opacity-0 z-[100] pointer-events-none scale-80\"\n        style={{ transition: `translate 0.1s ${POWER2_OUT}` }}\n      >\n        VIEW\n      </div>\n      <header className=\"absolute top-0 left-0 w-full z-20 flex justify-between items-center p-6 md:p-8 pointer-events-none\">\n        <div className=\"flex-1 hidden sm:block\"></div>\n        <div className=\"flex-1 text-left sm:text-center\">\n          <h1 className=\"text-xl md:text-2xl font-medium tracking-wide drop-shadow-md pointer-events-auto cursor-pointer hover:opacity-70 transition\">\n            Fluid Glass\n          </h1>\n        </div>\n        <div className=\"flex-1 hidden sm:block\"></div>\n      </header>\n      <main\n        ref={sliderRef}\n        className=\"flex h-full\"\n        style={{ width: `${slidesData.length * 100}%` }}\n      >\n        {slidesData.map((slide) => (\n          <section\n            key={slide.id}\n            className=\"relative flex items-end pb-24 md:pb-32 px-6 md:px-12 bg-cover bg-center h-full\"\n            style={{\n              backgroundImage: `url(${slide.image})`,\n              width: `${100 / slidesData.length}%`,\n            }}\n          >\n            <div className=\"absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-black/40\"></div>\n\n            <div className=\"relative z-10 w-full\">\n              <h2 className=\"text-3xl md:text-5xl lg:text-[4rem] font-semibold leading-[1.1] mb-8 md:mb-14 tracking-tight max-w-4xl drop-shadow-xl\">\n                {slide.title}\n              </h2>\n              <p className=\"text-[10px] md:text-[11px] uppercase tracking-[0.2em] font-semibold text-gray-300 drop-shadow-md\">\n                {slide.subtitle}\n              </p>\n            </div>\n          </section>\n        ))}\n      </main>\n      <div className=\"absolute bottom-0 left-0 w-full pointer-events-none z-20 pb-6 md:pb-10 px-6 md:px-12 flex items-end\">\n        <div className=\"w-full flex flex-col sm:flex-row sm:justify-between items-start sm:items-center gap-6 sm:gap-0\">\n          <div className=\"w-full sm:flex-1 sm:mr-12 h-[2px] bg-gray-600/30 rounded-full overflow-hidden\">\n            <div ref={progressRef} className=\"h-full bg-white w-0\" />\n          </div>\n          <div className=\"flex items-center justify-between w-full sm:w-auto gap-8 pointer-events-auto shrink-0\">\n            <div className=\"flex gap-2\">\n              <button\n                onClick={prevSlide}\n                disabled={isFirstSlide}\n                onMouseEnter={hideCursor}\n                onMouseLeave={showCursor}\n                className={`w-10 h-10 md:w-12 md:h-12 rounded-full backdrop-blur-sm border flex items-center justify-center transition-all ${\n                  isFirstSlide\n                    ? \"bg-black/20 border-white/10 opacity-30 cursor-not-allowed\"\n                    : \"bg-black/40 border-white/20 hover:bg-white/20 hover:scale-105 cursor-pointer\"\n                }`}\n              >\n                <svg\n                  className=\"w-5 h-5 text-white\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M15 19l-7-7 7-7\"\n                  />\n                </svg>\n              </button>\n\n              <button\n                onClick={nextSlide}\n                disabled={isLastSlide}\n                onMouseEnter={hideCursor}\n                onMouseLeave={showCursor}\n                className={`w-10 h-10 md:w-12 md:h-12 rounded-full backdrop-blur-sm border flex items-center justify-center transition-all ${\n                  isLastSlide\n                    ? \"bg-black/20 border-white/10 opacity-30 cursor-not-allowed\"\n                    : \"bg-black/40 border-white/20 hover:bg-white/20 hover:scale-105 cursor-pointer\"\n                }`}\n              >\n                <svg\n                  className=\"w-5 h-5 text-white\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M9 5l7 7-7 7\"\n                  />\n                </svg>\n              </button>\n            </div>\n\n            <div className=\"text-[11px] md:text-[13px] font-semibold tracking-widest font-mono drop-shadow-md\">\n              <span>0{currentIndex + 1}</span> / 0{slidesData.length}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/* ============================================================================\n   UNIFIED CAROUSEL DEFAULT EXPORT (Support variant prop)\n   ============================================================================ */\n\nexport type CarouselVariant = \"image\" | \"hero\" | \"thumbnails\" | \"radial\" | \"glass\" | \"fluid\";\n\n/**\n * Discriminated on `variant`, so each one takes exactly the props its\n * carousel takes. The old shape accepted `any[]` for `slides`/`items` plus a\n * catch-all index signature, which meant nothing you passed was checked and\n * a slide array meant for one variant compiled fine against another.\n */\nexport type CarouselComponentProps =\n  | ({ variant?: \"image\"; className?: string } & Omit<\n      ImageCarouselProps,\n      \"rootClassName\"\n    >)\n  | ({ variant: \"hero\" } & HeroCarouselProps)\n  | ({ variant: \"thumbnails\" } & CustomCarouselProps)\n  | ({ variant: \"radial\" } & RadialCarouselProps)\n  | ({ variant: \"glass\" | \"fluid\" } & FluidGlassSliderProps);\n\nexport default function Carousel(props: CarouselComponentProps) {\n  if (props.variant === \"glass\" || props.variant === \"fluid\") {\n    return <FluidGlassSlider {...props} />;\n  }\n  if (props.variant === \"hero\") {\n    return <HeroCarousel {...props} />;\n  }\n  if (props.variant === \"thumbnails\") {\n    return <CustomCarousel {...props} />;\n  }\n  if (props.variant === \"radial\") {\n    return <RadialCarousel {...props} />;\n  }\n  return <ImageCarousel {...props} rootClassName={props.className} />;\n}\n",
      "type": "registry:component",
      "target": "components/custom/carousel.tsx"
    }
  ],
  "docs": "Named exports: ImageCarousel, HeroCarousel, CustomCarousel. All three take an images array; HeroCarousel adds autoplay and CustomCarousel adds thumbnail navigation. Built on next/image, so remote hosts must be allowed in next.config.",
  "categories": [
    "media",
    "navigation"
  ],
  "type": "registry:component"
}
