{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A CoverflowCarousel component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface CoverflowCarouselItem {\n  alt?: string;\n  content?: ReactNode;\n  id: string;\n  image?: string;\n}\n\nexport interface CoverflowCarouselProps {\n  autoplay?: boolean;\n  autoplayDelay?: number;\n  className?: string;\n  depth?: number;\n  index?: number;\n  inverted?: boolean;\n  items: CoverflowCarouselItem[];\n  loop?: boolean;\n  onIndexChange?: (index: number) => void;\n  rotation?: number;\n  scaleStep?: number;\n  spacing?: number;\n}\n\ninterface DragInfo {\n  offset: { x: number; y: number };\n  velocity: { x: number; y: number };\n}\n\nconst DEFAULT_DEPTH = 180;\nconst DEFAULT_ROTATION = 45;\nconst DEFAULT_SPACING = 220;\nconst DEFAULT_SCALE_STEP = 0.15;\nconst DEFAULT_AUTOPLAY_DELAY = 4000;\nconst SWIPE_VELOCITY_THRESHOLD = 500;\nconst SWIPE_DISTANCE_THRESHOLD = 80;\nconst MAX_VISIBLE_OFFSET = 3;\nconst MIN_SCALE = 0.4;\n\nconst CoverflowCarousel = ({\n  items,\n  index: indexProp,\n  onIndexChange,\n  inverted = false,\n  depth = DEFAULT_DEPTH,\n  rotation = DEFAULT_ROTATION,\n  spacing = DEFAULT_SPACING,\n  scaleStep = DEFAULT_SCALE_STEP,\n  loop = false,\n  autoplay = false,\n  autoplayDelay = DEFAULT_AUTOPLAY_DELAY,\n  className,\n}: CoverflowCarouselProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalIndex, setInternalIndex] = useState(0);\n  const isControlled = indexProp !== undefined;\n  const activeIndex = isControlled ? indexProp : internalIndex;\n  const [isPaused, setIsPaused] = useState(false);\n  const total = items.length;\n\n  const goTo = useCallback(\n    (next: number) => {\n      const clamped = loop\n        ? ((next % total) + total) % total\n        : Math.min(Math.max(next, 0), total - 1);\n\n      if (!isControlled) {\n        setInternalIndex(clamped);\n      }\n      onIndexChange?.(clamped);\n    },\n    [isControlled, loop, onIndexChange, total]\n  );\n\n  useEffect(() => {\n    if (!autoplay || shouldReduceMotion || isPaused || total <= 1) {\n      return;\n    }\n\n    const timer = setInterval(() => {\n      goTo(activeIndex + 1);\n    }, autoplayDelay);\n\n    return () => clearInterval(timer);\n  }, [\n    autoplay,\n    shouldReduceMotion,\n    isPaused,\n    activeIndex,\n    autoplayDelay,\n    goTo,\n    total,\n  ]);\n\n  useEffect(() => {\n    const handleVisibility = () => {\n      setIsPaused(document.hidden);\n    };\n\n    document.addEventListener(\"visibilitychange\", handleVisibility);\n    return () =>\n      document.removeEventListener(\"visibilitychange\", handleVisibility);\n  }, []);\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      goTo(activeIndex + 1);\n    } else if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      goTo(activeIndex - 1);\n    }\n  };\n\n  const handleDragEnd = (\n    _event: MouseEvent | TouchEvent | PointerEvent,\n    info: DragInfo\n  ) => {\n    const isSwipeLeft =\n      info.offset.x < -SWIPE_DISTANCE_THRESHOLD ||\n      info.velocity.x < -SWIPE_VELOCITY_THRESHOLD;\n    const isSwipeRight =\n      info.offset.x > SWIPE_DISTANCE_THRESHOLD ||\n      info.velocity.x > SWIPE_VELOCITY_THRESHOLD;\n\n    if (isSwipeLeft) {\n      goTo(activeIndex + 1);\n    } else if (isSwipeRight) {\n      goTo(activeIndex - 1);\n    }\n  };\n\n  const dragEndRef = useRef(handleDragEnd);\n  dragEndRef.current = handleDragEnd;\n\n  return (\n    <div\n      aria-label=\"Coverflow carousel\"\n      aria-roledescription=\"carousel\"\n      className={cn(\"relative w-full select-none outline-none\", className)}\n      onBlur={() => setIsPaused(false)}\n      onFocus={() => setIsPaused(true)}\n      onKeyDown={handleKeyDown}\n      onMouseEnter={() => setIsPaused(true)}\n      onMouseLeave={() => setIsPaused(false)}\n      role=\"region\"\n      style={{ perspective: shouldReduceMotion ? undefined : 1200 }}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: this WAI-ARIA APG carousel widget intentionally accepts focus so ArrowLeft/ArrowRight can move slides while the region is focused (in addition to the Previous/Next buttons below); removing tabIndex would remove that keyboard-navigation path entirely.\n      tabIndex={0}\n    >\n      <motion.div\n        className=\"relative mx-auto flex h-[320px] items-center justify-center\"\n        drag={total > 1 && !shouldReduceMotion ? \"x\" : false}\n        dragConstraints={{ left: 0, right: 0 }}\n        dragElastic={0.12}\n        onDragEnd={(event, info) => dragEndRef.current(event, info)}\n        style={{ transformStyle: \"preserve-3d\" }}\n      >\n        {items.map((item, i) => {\n          const offset = i - activeIndex;\n          const isVisible = Math.abs(offset) <= MAX_VISIBLE_OFFSET;\n          const isActive = offset === 0;\n\n          if (!isVisible) {\n            return null;\n          }\n\n          const direction = inverted ? -1 : 1;\n          const rotateY = shouldReduceMotion\n            ? 0\n            : direction * -offset * rotation;\n          const translateX = offset * spacing;\n          const translateZ = shouldReduceMotion ? 0 : -Math.abs(offset) * depth;\n          const scale = Math.max(1 - Math.abs(offset) * scaleStep, MIN_SCALE);\n\n          return (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: isActive ? 1 : 0, x: translateX }\n                  : {\n                      opacity: 1,\n                      rotateY,\n                      scale,\n                      x: translateX,\n                      z: translateZ,\n                    }\n              }\n              aria-hidden={!isActive}\n              className=\"absolute h-[220px] w-[280px] overflow-hidden rounded-2xl border border-foreground/10 bg-background shadow-xl\"\n              key={item.id}\n              style={{\n                transformStyle: \"preserve-3d\",\n                zIndex: total - Math.abs(offset),\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n              }\n            >\n              {item.image ? (\n                <img\n                  alt={item.alt ?? \"\"}\n                  className=\"h-full w-full object-cover\"\n                  draggable={false}\n                  src={item.image}\n                />\n              ) : (\n                <div className=\"flex h-full w-full items-center justify-center p-4 text-sm\">\n                  {item.content}\n                </div>\n              )}\n            </motion.div>\n          );\n        })}\n      </motion.div>\n\n      <div className=\"mt-4 flex items-center justify-center gap-4\">\n        <SmoothButton\n          aria-label=\"Previous slide\"\n          disabled={!loop && activeIndex === 0}\n          onClick={() => goTo(activeIndex - 1)}\n          shape=\"pill\"\n          size=\"sm\"\n          variant=\"outline\"\n        >\n          Prev\n        </SmoothButton>\n        <SmoothButton\n          aria-label=\"Next slide\"\n          disabled={!loop && activeIndex === total - 1}\n          onClick={() => goTo(activeIndex + 1)}\n          shape=\"pill\"\n          size=\"sm\"\n          variant=\"outline\"\n        >\n          Next\n        </SmoothButton>\n      </div>\n\n      <div aria-live=\"polite\" className=\"sr-only\">\n        {`Slide ${activeIndex + 1} of ${total}`}\n      </div>\n    </div>\n  );\n};\n\nexport default CoverflowCarousel;\n","path":"index.tsx","target":"components/smoothui/coverflow-carousel/index.tsx","type":"registry:ui"}],"name":"coverflow-carousel","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Coverflow Carousel","type":"registry:ui"}