{
  "name": "apple-cards-carousel",
  "type": "registry:ui",
  "dependencies": [
    "@tabler/icons-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.aceternity.com/registry/use-outside-click.json"
  ],
  "files": [
    {
      "path": "components/ui/apple-cards-carousel.tsx",
      "content": "\"use client\";\nimport React, {\n  useEffect,\n  useRef,\n  useState,\n  createContext,\n  useContext,\n} from \"react\";\nimport {\n  IconArrowNarrowLeft,\n  IconArrowNarrowRight,\n  IconX,\n} from \"@tabler/icons-react\";\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport Image, { ImageProps } from \"next/image\";\nimport { useOutsideClick } from \"@/hooks/use-outside-click\";\n\ninterface CarouselProps {\n  items: JSX.Element[];\n  initialScroll?: number;\n}\n\ntype Card = {\n  src: string;\n  title: string;\n  category: string;\n  content: React.ReactNode;\n};\n\nexport const CarouselContext = createContext<{\n  onCardClose: (index: number) => void;\n  currentIndex: number;\n}>({\n  onCardClose: () => {},\n  currentIndex: 0,\n});\n\nexport const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {\n  const carouselRef = React.useRef<HTMLDivElement>(null);\n  const [canScrollLeft, setCanScrollLeft] = React.useState(false);\n  const [canScrollRight, setCanScrollRight] = React.useState(true);\n  const [currentIndex, setCurrentIndex] = useState(0);\n\n  useEffect(() => {\n    if (carouselRef.current) {\n      carouselRef.current.scrollLeft = initialScroll;\n      checkScrollability();\n    }\n  }, [initialScroll]);\n\n  const checkScrollability = () => {\n    if (carouselRef.current) {\n      const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;\n      setCanScrollLeft(scrollLeft > 0);\n      setCanScrollRight(scrollLeft < scrollWidth - clientWidth);\n    }\n  };\n\n  const scrollLeft = () => {\n    if (carouselRef.current) {\n      carouselRef.current.scrollBy({ left: -300, behavior: \"smooth\" });\n    }\n  };\n\n  const scrollRight = () => {\n    if (carouselRef.current) {\n      carouselRef.current.scrollBy({ left: 300, behavior: \"smooth\" });\n    }\n  };\n\n  const handleCardClose = (index: number) => {\n    if (carouselRef.current) {\n      const cardWidth = isMobile() ? 230 : 384; // (md:w-96)\n      const gap = isMobile() ? 4 : 8;\n      const scrollPosition = (cardWidth + gap) * (index + 1);\n      carouselRef.current.scrollTo({\n        left: scrollPosition,\n        behavior: \"smooth\",\n      });\n      setCurrentIndex(index);\n    }\n  };\n\n  const isMobile = () => {\n    return window && window.innerWidth < 768;\n  };\n\n  return (\n    <CarouselContext.Provider\n      value={{ onCardClose: handleCardClose, currentIndex }}\n    >\n      <div className=\"relative w-full\">\n        <div\n          className=\"flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20\"\n          ref={carouselRef}\n          onScroll={checkScrollability}\n        >\n          <div\n            className={cn(\n              \"absolute right-0 z-[1000] h-auto w-[5%] overflow-hidden bg-gradient-to-l\",\n            )}\n          ></div>\n\n          <div\n            className={cn(\n              \"flex flex-row justify-start gap-4 pl-4\",\n              \"mx-auto max-w-7xl\", // remove max-w-4xl if you want the carousel to span the full width of its container\n            )}\n          >\n            {items.map((item, index) => (\n              <motion.div\n                initial={{\n                  opacity: 0,\n                  y: 20,\n                }}\n                animate={{\n                  opacity: 1,\n                  y: 0,\n                  transition: {\n                    duration: 0.5,\n                    delay: 0.2 * index,\n                    ease: \"easeOut\",\n                  },\n                }}\n                key={\"card\" + index}\n                className=\"rounded-3xl last:pr-[5%] md:last:pr-[33%]\"\n              >\n                {item}\n              </motion.div>\n            ))}\n          </div>\n        </div>\n        <div className=\"mr-10 flex justify-end gap-2\">\n          <button\n            className=\"relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 disabled:opacity-50\"\n            onClick={scrollLeft}\n            disabled={!canScrollLeft}\n          >\n            <IconArrowNarrowLeft className=\"h-6 w-6 text-gray-500\" />\n          </button>\n          <button\n            className=\"relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 disabled:opacity-50\"\n            onClick={scrollRight}\n            disabled={!canScrollRight}\n          >\n            <IconArrowNarrowRight className=\"h-6 w-6 text-gray-500\" />\n          </button>\n        </div>\n      </div>\n    </CarouselContext.Provider>\n  );\n};\n\nexport const Card = ({\n  card,\n  index,\n  layout = false,\n}: {\n  card: Card;\n  index: number;\n  layout?: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const { onCardClose, currentIndex } = useContext(CarouselContext);\n\n  useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        handleClose();\n      }\n    }\n\n    if (open) {\n      document.body.style.overflow = \"hidden\";\n    } else {\n      document.body.style.overflow = \"auto\";\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, [open]);\n\n  useOutsideClick(containerRef, () => handleClose());\n\n  const handleOpen = () => {\n    setOpen(true);\n  };\n\n  const handleClose = () => {\n    setOpen(false);\n    onCardClose(index);\n  };\n\n  return (\n    <>\n      <AnimatePresence>\n        {open && (\n          <div className=\"fixed inset-0 z-50 h-screen overflow-auto\">\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              className=\"fixed inset-0 h-full w-full bg-black/80 backdrop-blur-lg\"\n            />\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              ref={containerRef}\n              layoutId={layout ? `card-${card.title}` : undefined}\n              className=\"relative z-[60] mx-auto my-10 h-fit max-w-5xl rounded-3xl bg-white p-4 font-sans md:p-10 dark:bg-neutral-900\"\n            >\n              <button\n                className=\"sticky top-4 right-0 ml-auto flex h-8 w-8 items-center justify-center rounded-full bg-black dark:bg-white\"\n                onClick={handleClose}\n              >\n                <IconX className=\"h-6 w-6 text-neutral-100 dark:text-neutral-900\" />\n              </button>\n              <motion.p\n                layoutId={layout ? `category-${card.title}` : undefined}\n                className=\"text-base font-medium text-black dark:text-white\"\n              >\n                {card.category}\n              </motion.p>\n              <motion.p\n                layoutId={layout ? `title-${card.title}` : undefined}\n                className=\"mt-4 text-2xl font-semibold text-neutral-700 md:text-5xl dark:text-white\"\n              >\n                {card.title}\n              </motion.p>\n              <div className=\"py-10\">{card.content}</div>\n            </motion.div>\n          </div>\n        )}\n      </AnimatePresence>\n      <motion.button\n        layoutId={layout ? `card-${card.title}` : undefined}\n        onClick={handleOpen}\n        className=\"relative z-10 flex h-80 w-56 flex-col items-start justify-start overflow-hidden rounded-3xl bg-gray-100 md:h-[40rem] md:w-96 dark:bg-neutral-900\"\n      >\n        <div className=\"pointer-events-none absolute inset-x-0 top-0 z-30 h-full bg-gradient-to-b from-black/50 via-transparent to-transparent\" />\n        <div className=\"relative z-40 p-8\">\n          <motion.p\n            layoutId={layout ? `category-${card.category}` : undefined}\n            className=\"text-left font-sans text-sm font-medium text-white md:text-base\"\n          >\n            {card.category}\n          </motion.p>\n          <motion.p\n            layoutId={layout ? `title-${card.title}` : undefined}\n            className=\"mt-2 max-w-xs text-left font-sans text-xl font-semibold [text-wrap:balance] text-white md:text-3xl\"\n          >\n            {card.title}\n          </motion.p>\n        </div>\n        <BlurImage\n          src={card.src}\n          alt={card.title}\n          fill\n          className=\"absolute inset-0 z-10 object-cover\"\n        />\n      </motion.button>\n    </>\n  );\n};\n\nexport const BlurImage = ({\n  height,\n  width,\n  src,\n  className,\n  alt,\n  ...rest\n}: ImageProps) => {\n  const [isLoading, setLoading] = useState(true);\n  return (\n    <img\n      className={cn(\n        \"h-full w-full transition duration-300\",\n        isLoading ? \"blur-sm\" : \"blur-0\",\n        className,\n      )}\n      onLoad={() => setLoading(false)}\n      src={src as string}\n      width={width}\n      height={height}\n      loading=\"lazy\"\n      decoding=\"async\"\n      blurDataURL={typeof src === \"string\" ? src : undefined}\n      alt={alt ? alt : \"Background of a beautiful view\"}\n      {...rest}\n    />\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/apple-cards-carousel.tsx"
    },
    {
      "path": "hooks/use-outside-click.tsx",
      "content": "import React, { useEffect } from \"react\";\n\nexport const useOutsideClick = (\n  ref: React.RefObject<HTMLDivElement>,\n  callback: Function\n) => {\n  useEffect(() => {\n    const listener = (event: any) => {\n      // DO NOTHING if the element being clicked is the target element or their children\n      if (!ref.current || ref.current.contains(event.target)) {\n        return;\n      }\n      callback(event);\n    };\n\n    document.addEventListener(\"mousedown\", listener);\n    document.addEventListener(\"touchstart\", listener);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", listener);\n      document.removeEventListener(\"touchstart\", listener);\n    };\n  }, [ref, callback]);\n};\n",
      "type": "registry:hook",
      "target": "hooks/use-outside-click.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Apple Cards Carousel"
}