{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-carousel",
  "type": "registry:ui",
  "description": "Loading carousel component with smooth transitions and customizable content",
  "dependencies": [
    "embla-carousel-autoplay",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "carousel"
  ],
  "files": [
    {
      "path": "registry/default/ui/loading-carousel.tsx",
      "content": "// npm i embla-carousel-autoplay framer-motion lucide-react\n// npx shadcn@latest add carousel\n\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\"\nimport Image from \"next/image\"\nimport Autoplay from \"embla-carousel-autoplay\"\nimport { ChevronRight } from \"lucide-react\"\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n  type CarouselApi,\n} from \"@/components/ui/carousel\"\n\ninterface Tip {\n  text: string\n  image: string\n  url?: string\n}\n\ninterface LoadingCarouselProps {\n  tips?: Tip[]\n  className?: string\n  autoplayInterval?: number\n  showNavigation?: boolean\n  showIndicators?: boolean\n  showProgress?: boolean\n  aspectRatio?: \"video\" | \"square\" | \"wide\"\n  textPosition?: \"top\" | \"bottom\"\n  onTipChange?: (index: number) => void\n  backgroundTips?: boolean\n  backgroundGradient?: boolean\n  shuffleTips?: boolean\n}\n\nconst defaultTips: Tip[] = [\n  {\n    text: \"Backend snippets. Shadcn style headless components.. but for your backend.\",\n    image: \"/placeholders/cult-snips.png\",\n    url: \"https://www.newcult.co/backend\",\n  },\n  {\n    text: \"Create your first directory app today. AI batch scripts to process 100s of urls in seconds.\",\n    image: \"/placeholders/cult-dir.png\",\n    url: \"https://www.newcult.co/templates/cult-seo\",\n  },\n  {\n    text: \"Cult landing page template. Framer motion, shadcn, and tailwind.\",\n    image: \"/placeholders/cult-rune.png\",\n    url: \"https://www.newcult.co/templates/cult-landing-page\",\n  },\n  {\n    text: \"Vector embeddings, semantic search, and chat based vector retrieval on easy mode.\",\n    image: \"/placeholders/cult-manifest.png\",\n    url: \"https://www.newcult.co/templates/manifest\",\n  },\n  {\n    text: \"SEO analysis app. Scraping, analysis, insights, and AI recommendations.\",\n    image: \"/placeholders/cult-seo.png\",\n    url: \"https://www.newcult.co/templates/cult-seo\",\n  },\n]\n\nfunction shuffleArray<T>(array: T[]): T[] {\n  const shuffled = [...array]\n  for (let i = shuffled.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1))\n    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]\n  }\n  return shuffled\n}\n\nfunction getTipKey(tip: Tip): string {\n  return `${tip.text}-${tip.image}`\n}\n\nconst carouselVariants: Variants = {\n  enter: (direction: number) => ({\n    x: direction > 0 ? \"100%\" : \"-100%\",\n    opacity: 0,\n  }),\n  center: {\n    x: 0,\n    opacity: 1,\n  },\n  exit: (direction: number) => ({\n    x: direction < 0 ? \"100%\" : \"-100%\",\n    opacity: 0,\n  }),\n}\n\nconst textVariants: Variants = {\n  hidden: { opacity: 0, y: 20 },\n  visible: { opacity: 1, y: 0, transition: { delay: 0.3, duration: 0.5 } },\n}\n\nconst aspectRatioClasses = {\n  video: \"aspect-video\",\n  square: \"aspect-square\",\n  wide: \"aspect-[2/1]\",\n}\n\nexport function LoadingCarousel({\n  onTipChange,\n  className,\n  tips = defaultTips,\n  showProgress = true,\n  aspectRatio = \"video\",\n  showNavigation = false,\n  showIndicators = true,\n  backgroundTips = false,\n  textPosition = \"bottom\",\n  autoplayInterval = 4500,\n  backgroundGradient = false,\n  shuffleTips = false,\n}: LoadingCarouselProps) {\n  const [api, setApi] = useState<CarouselApi>()\n  const [current, setCurrent] = useState(0)\n  const [direction, setDirection] = useState(0)\n  const prefersReducedMotion = useReducedMotion()\n  const [displayTips] = useState(() =>\n    shuffleTips ? shuffleArray(tips) : tips\n  )\n\n  const autoplay = useMemo(\n    () =>\n      Autoplay({\n        delay: autoplayInterval,\n        stopOnInteraction: false,\n      }),\n    [autoplayInterval]\n  )\n\n  useEffect(() => {\n    if (!api) {\n      return\n    }\n\n    setCurrent(api.selectedScrollSnap())\n    setDirection(\n      api.scrollSnapList().indexOf(api.selectedScrollSnap()) - current\n    )\n\n    const onSelect = () => {\n      const newIndex = api.selectedScrollSnap()\n      setCurrent(newIndex)\n      setDirection(api.scrollSnapList().indexOf(newIndex) - current)\n      onTipChange?.(newIndex)\n    }\n\n    api.on(\"select\", onSelect)\n\n    return () => {\n      api.off(\"select\", onSelect)\n    }\n  }, [api, current, onTipChange])\n\n  const handleSelect = useCallback(\n    (index: number) => {\n      api?.scrollTo(index)\n    },\n    [api]\n  )\n\n  return (\n    <motion.div\n      initial={prefersReducedMotion ? false : { opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={\n        prefersReducedMotion\n          ? { duration: 0 }\n          : { duration: 0.8, ease: \"easeOut\" }\n      }\n      className={cn(\n        \"bg-muted mx-auto w-full max-w-6xl overflow-hidden rounded-xl shadow-[0px_1px_1px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_rgba(255,252,240,0.5)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.1)_inset,0px_0px_1px_0px_rgba(28,27,26,0.5)]\",\n        className\n      )}\n    >\n      <div className=\"w-full overflow-hidden rounded-xl\">\n        <Carousel\n          setApi={setApi}\n          plugins={[autoplay]}\n          className=\"relative w-full\"\n          opts={{\n            loop: true,\n          }}\n        >\n          <CarouselContent>\n            <AnimatePresence initial={false} custom={direction}>\n              {(displayTips || []).map((tip) => (\n                <CarouselItem key={getTipKey(tip)} className=\"min-w-0\">\n                  <motion.div\n                    variants={carouselVariants}\n                    initial={prefersReducedMotion ? false : \"enter\"}\n                    animate=\"center\"\n                    exit={prefersReducedMotion ? undefined : \"exit\"}\n                    custom={direction}\n                    transition={\n                      prefersReducedMotion\n                        ? { duration: 0 }\n                        : { duration: 0.8, ease: \"easeInOut\" }\n                    }\n                    className={`relative ${aspectRatioClasses[aspectRatio]} w-full overflow-hidden`}\n                  >\n                    <Image\n                      src={tip.image}\n                      alt={`Visual representation for tip: ${tip.text}`}\n                      fill\n                      className=\"object-cover\"\n                      priority\n                    />\n                    {backgroundGradient && (\n                      <div className=\"absolute inset-0 bg-linear-to-t from-black/90 via-black/20 to-transparent\" />\n                    )}\n\n                    {backgroundTips ? (\n                      <motion.div\n                        variants={textVariants}\n                        initial={prefersReducedMotion ? false : \"hidden\"}\n                        animate=\"visible\"\n                        className={`absolute ${\n                          textPosition === \"top\" ? \"top-0\" : \"bottom-0\"\n                        } right-0 left-0 min-w-0 p-4 sm:p-6 md:p-8`}\n                      >\n                        {displayTips[current]?.url ? (\n                          <a\n                            href={displayTips[current]?.url}\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            className=\"block min-w-0 rounded-sm focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:outline-none\"\n                          >\n                            <p className=\"text-center text-base leading-relaxed font-medium tracking-tight text-pretty wrap-break-word text-white sm:text-lg md:text-left md:text-xl lg:text-2xl lg:font-bold\">\n                              {tip.text}\n                            </p>\n                          </a>\n                        ) : (\n                          <p className=\"text-center text-base leading-relaxed font-medium tracking-tight text-pretty wrap-break-word text-white sm:text-lg md:text-left md:text-xl lg:text-2xl lg:font-bold\">\n                            {tip.text}\n                          </p>\n                        )}\n                      </motion.div>\n                    ) : null}\n                  </motion.div>\n                </CarouselItem>\n              ))}\n            </AnimatePresence>\n          </CarouselContent>\n          {showNavigation && (\n            <>\n              <CarouselPrevious className=\"absolute top-1/2 left-2 h-10 w-10 -translate-y-1/2 transition-transform active:scale-[0.96]\" />\n              <CarouselNext className=\"absolute top-1/2 right-2 h-10 w-10 -translate-y-1/2 transition-transform active:scale-[0.96]\" />\n            </>\n          )}\n        </Carousel>\n        <div\n          className={cn(\n            \"bg-muted p-4 sm:p-5\",\n            showIndicators && !backgroundTips ? \"lg:px-4 lg:py-3\" : \"\"\n          )}\n        >\n          <div\n            className={cn(\n              \"flex min-w-0 flex-col items-start justify-between gap-3 sm:flex-row sm:items-center\",\n              showIndicators && !backgroundTips\n                ? \"sm:flex-col sm:items-start\"\n                : \"\"\n            )}\n          >\n            {showIndicators && (\n              <div className=\"flex w-full gap-2 overflow-x-auto pb-1 sm:pb-0\">\n                {(displayTips || []).map((tip, index) => {\n                  const isActive = index === current\n                  const isComplete = index < current\n\n                  return (\n                    <button\n                      key={getTipKey(tip)}\n                      type=\"button\"\n                      className=\"focus-visible:ring-ring focus-visible:ring-offset-muted flex h-10 min-w-8 flex-1 items-center rounded-full transition-transform focus-visible:ring-2 focus-visible:ring-offset-4 focus-visible:outline-none active:scale-[0.96] sm:min-w-0 sm:shrink\"\n                      onClick={() => handleSelect(index)}\n                      aria-label={`Go to tip ${index + 1}`}\n                      aria-current={isActive ? \"true\" : undefined}\n                    >\n                      <span className=\"bg-foreground/15 relative h-1 w-full overflow-hidden rounded-full\">\n                        {showProgress ? (\n                          isComplete ? (\n                            <span className=\"bg-foreground/70 absolute inset-0 rounded-full\" />\n                          ) : isActive ? (\n                            <motion.span\n                              key={current}\n                              initial={{ scaleX: prefersReducedMotion ? 1 : 0 }}\n                              animate={{ scaleX: 1 }}\n                              transition={\n                                prefersReducedMotion\n                                  ? { duration: 0 }\n                                  : {\n                                      duration: autoplayInterval / 1000,\n                                      ease: \"linear\",\n                                    }\n                              }\n                              className=\"bg-foreground/70 absolute inset-0 origin-left rounded-full\"\n                            />\n                          ) : null\n                        ) : (\n                          <span\n                            className={cn(\n                              \"bg-foreground/70 absolute inset-0 origin-left rounded-full transition-transform\",\n                              prefersReducedMotion\n                                ? \"duration-0\"\n                                : \"duration-300 ease-out\",\n                              isActive ? \"scale-x-100\" : \"scale-x-0\"\n                            )}\n                          />\n                        )}\n                      </span>\n                    </button>\n                  )\n                })}\n              </div>\n            )}\n            <div className=\"text-primary flex min-w-0 items-center gap-2\">\n              {backgroundTips ? (\n                <span className=\"text-sm font-medium whitespace-nowrap tabular-nums\">\n                  Tip {current + 1}/{displayTips?.length || 0}\n                </span>\n              ) : (\n                <div className=\"max-w-full min-w-0\">\n                  {displayTips[current]?.url ? (\n                    <a\n                      href={displayTips[current]?.url}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"focus-visible:ring-ring focus-visible:ring-offset-muted block max-w-full rounded-sm text-base leading-tight font-medium tracking-tight text-pretty wrap-break-word focus-visible:ring-2 focus-visible:ring-offset-4 focus-visible:outline-none lg:text-xl xl:font-semibold\"\n                    >\n                      {displayTips[current]?.text}\n                    </a>\n                  ) : (\n                    <span className=\"block max-w-full text-base leading-tight font-medium tracking-tight text-pretty wrap-break-word lg:text-2xl xl:font-semibold\">\n                      {displayTips[current]?.text}\n                    </span>\n                  )}\n                </div>\n              )}\n              {backgroundTips && (\n                <ChevronRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </motion.div>\n  )\n}\n\nexport default LoadingCarousel\n",
      "type": "registry:ui"
    }
  ]
}