{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-carousel",
  "title": "Carousel",
  "description": "A component for cycling through elements with smooth transitions.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "components/core/carousel.tsx",
      "content": "'use client';\nimport {\n  Children,\n  ReactNode,\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type FocusEvent,\n  type KeyboardEvent,\n  type PointerEvent,\n} from 'react';\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  MotionConfig,\n  MotionValue,\n  PanInfo,\n  Transition,\n  useMotionValue,\n  useTransform,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronUpIcon,\n  PauseIcon,\n  PlayIcon,\n} from '@/registry/ndeko/lib/icons';\n\nexport type CarouselOrientation = 'horizontal' | 'vertical';\n\nexport type CarouselContextType = {\n  index: number;\n  setIndex: (newIndex: number) => void;\n  itemsCount: number;\n  setItemsCount: (newItemsCount: number) => void;\n  disableDrag: boolean;\n  orientation: CarouselOrientation;\n  loop: boolean;\n  slidesPerView: number | undefined;\n  lastIndex: number;\n  canGoNext: boolean;\n  canGoPrevious: boolean;\n  next: () => void;\n  previous: () => void;\n  autoplay: boolean;\n  isAutoplayPaused: boolean;\n  toggleAutoplay: () => void;\n  autoplayProgress: MotionValue<number>;\n};\n\ntype CarouselInteractionContextType = {\n  carouselId: string;\n  setHovered: (isHovered: boolean) => void;\n  setKeyboardFocused: (isFocused: boolean) => void;\n  setDragging: (isDragging: boolean) => void;\n};\n\nconst CarouselContext = createContext<CarouselContextType | undefined>(\n  undefined\n);\n\nconst CarouselInteractionContext = createContext<\n  CarouselInteractionContextType | undefined\n>(undefined);\n\nfunction useCarousel() {\n  const context = useContext(CarouselContext);\n  if (!context) {\n    throw new Error('useCarousel must be used within an CarouselProvider');\n  }\n  return context;\n}\n\nfunction useCarouselInteraction() {\n  const context = useContext(CarouselInteractionContext);\n  if (!context) {\n    throw new Error('Carousel parts must be used within a Carousel');\n  }\n  return context;\n}\n\nexport type CarouselProviderProps = {\n  children: ReactNode;\n  initialIndex?: number;\n  onIndexChange?: (newIndex: number) => void;\n  disableDrag?: boolean;\n  orientation?: CarouselOrientation;\n  loop?: boolean;\n  slidesPerView?: number;\n  autoplay?: boolean;\n  autoplayDelay?: number;\n};\n\nconst TRAVEL_TRANSITION: Transition = {\n  type: 'spring',\n  bounce: 0,\n  duration: 0.3,\n};\n\nfunction ReducedMotionConfig({ children }: { children: ReactNode }) {\n  const prefersReducedMotion = usePrefersReducedMotion();\n  if (!prefersReducedMotion) {\n    return children;\n  }\n  return <MotionConfig reducedMotion='always'>{children}</MotionConfig>;\n}\n\nfunction lastIndexOf(itemsCount: number, slidesPerView: number | undefined) {\n  if (slidesPerView === undefined) {\n    return Math.max(0, itemsCount - 1);\n  }\n  const hiddenItems = itemsCount - slidesPerView;\n  return Math.max(0, Math.ceil(hiddenItems));\n}\n\nfunction isDocumentVisible() {\n  if (typeof document === 'undefined') {\n    return true;\n  }\n  return document.visibilityState === 'visible';\n}\n\nfunction useDocumentVisibility() {\n  const [isVisible, setIsVisible] = useState(true);\n\n  useEffect(() => {\n    const handleVisibilityChange = () => {\n      setIsVisible(isDocumentVisible());\n    };\n    document.addEventListener('visibilitychange', handleVisibilityChange, {\n      passive: true,\n    });\n    return () => {\n      document.removeEventListener('visibilitychange', handleVisibilityChange);\n    };\n  }, []);\n\n  return isVisible;\n}\n\nfunction CarouselProvider({\n  children,\n  initialIndex = 0,\n  onIndexChange,\n  disableDrag = false,\n  orientation = 'horizontal',\n  loop = false,\n  slidesPerView,\n  autoplay = false,\n  autoplayDelay = 5000,\n}: CarouselProviderProps) {\n  const [index, setIndex] = useState<number>(initialIndex);\n  const [syncedInitialIndex, setSyncedInitialIndex] =\n    useState<number>(initialIndex);\n  const [itemsCount, setItemsCount] = useState<number>(0);\n  const [isAutoplayPaused, setIsAutoplayPaused] = useState(false);\n  const [isHovered, setHovered] = useState(false);\n  const [isKeyboardFocused, setKeyboardFocused] = useState(false);\n  const [isDragging, setDragging] = useState(false);\n  const isPageVisible = useDocumentVisibility();\n  const prefersReducedMotion = usePrefersReducedMotion();\n  const autoplayProgress = useMotionValue(0);\n  const carouselId = useId();\n\n  const hasNewInitialIndex = initialIndex !== syncedInitialIndex;\n  if (hasNewInitialIndex) {\n    setSyncedInitialIndex(initialIndex);\n    setIndex(initialIndex);\n  }\n\n  const handleSetIndex = (newIndex: number) => {\n    setIndex(newIndex);\n    onIndexChange?.(newIndex);\n  };\n\n  const lastIndex = lastIndexOf(itemsCount, slidesPerView);\n  const hasPrevious = index > 0;\n  const hasNext = index < lastIndex;\n  const canGoPrevious = loop || hasPrevious;\n  const canGoNext = loop || hasNext;\n\n  const next = () => {\n    if (hasNext) {\n      handleSetIndex(index + 1);\n      return;\n    }\n    if (loop) {\n      handleSetIndex(0);\n    }\n  };\n\n  const previous = () => {\n    if (hasPrevious) {\n      handleSetIndex(index - 1);\n      return;\n    }\n    if (loop) {\n      handleSetIndex(lastIndex);\n    }\n  };\n\n  const hasSeveralPositions = lastIndex > 0;\n  const isMotionAllowed = !prefersReducedMotion;\n  const isAutoplayAvailable = autoplay && isMotionAllowed;\n  const isAutoplayOn = isAutoplayAvailable && hasSeveralPositions;\n  const isUserPlaying = !isAutoplayPaused;\n  const isUndisturbed = !isHovered && !isKeyboardFocused;\n  const isIdle = isUndisturbed && !isDragging;\n  const isAutoplayRunning = isAutoplayOn && isUserPlaying && isIdle;\n  const shouldAdvance = isAutoplayRunning && isPageVisible;\n\n  useEffect(() => {\n    autoplayProgress.jump(0);\n  }, [index, autoplayProgress]);\n\n  useEffect(() => {\n    if (!shouldAdvance) {\n      return;\n    }\n    const remaining = 1 - autoplayProgress.get();\n    const advance = () => {\n      if (index < lastIndex) {\n        handleSetIndex(index + 1);\n        return;\n      }\n      handleSetIndex(0);\n    };\n    const controls = animate(autoplayProgress, 1, {\n      duration: (remaining * autoplayDelay) / 1000,\n      ease: 'linear',\n      onComplete: advance,\n    });\n    return () => controls.stop();\n    // handleSetIndex is recreated on each render; the timer restarts only when its inputs change\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [shouldAdvance, index, lastIndex, autoplayDelay, autoplayProgress]);\n\n  const toggleAutoplay = () => {\n    setIsAutoplayPaused(!isAutoplayPaused);\n  };\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        index,\n        setIndex: handleSetIndex,\n        itemsCount,\n        setItemsCount,\n        disableDrag,\n        orientation,\n        loop,\n        slidesPerView,\n        lastIndex,\n        canGoNext,\n        canGoPrevious,\n        next,\n        previous,\n        autoplay: isAutoplayAvailable,\n        isAutoplayPaused,\n        toggleAutoplay,\n        autoplayProgress,\n      }}\n    >\n      <CarouselInteractionContext.Provider\n        value={{ carouselId, setHovered, setKeyboardFocused, setDragging }}\n      >\n        {children}\n      </CarouselInteractionContext.Provider>\n    </CarouselContext.Provider>\n  );\n}\n\nexport type CarouselProps = {\n  children: ReactNode;\n  className?: string;\n  initialIndex?: number;\n  index?: number;\n  onIndexChange?: (newIndex: number) => void;\n  disableDrag?: boolean;\n  orientation?: CarouselOrientation;\n  loop?: boolean;\n  slidesPerView?: number;\n  autoplay?: boolean;\n  autoplayDelay?: number;\n  'aria-label'?: string;\n};\n\nfunction isEditableTarget(target: EventTarget) {\n  if (!(target instanceof HTMLElement)) {\n    return false;\n  }\n  const isField = target.matches('input, textarea, select');\n  return isField || target.isContentEditable;\n}\n\nfunction stepOfKey(key: string, orientation: CarouselOrientation) {\n  const isVertical = orientation === 'vertical';\n  if (isVertical) {\n    if (key === 'ArrowUp') {\n      return -1;\n    }\n    if (key === 'ArrowDown') {\n      return 1;\n    }\n    return 0;\n  }\n  if (key === 'ArrowLeft') {\n    return -1;\n  }\n  if (key === 'ArrowRight') {\n    return 1;\n  }\n  return 0;\n}\n\ntype CarouselRootProps = {\n  children: ReactNode;\n  className?: string;\n  'aria-label'?: string;\n};\n\nfunction CarouselRoot({\n  children,\n  className,\n  'aria-label': ariaLabel,\n}: CarouselRootProps) {\n  const { orientation, next, previous } = useCarousel();\n  const { setHovered, setKeyboardFocused } = useCarouselInteraction();\n\n  const handlePointerEnter = (event: PointerEvent<HTMLDivElement>) => {\n    const isMouse = event.pointerType === 'mouse';\n    if (isMouse) {\n      setHovered(true);\n    }\n  };\n\n  const handleFocus = (event: FocusEvent<HTMLDivElement>) => {\n    const isKeyboardFocus = event.target.matches(':focus-visible');\n    if (isKeyboardFocus) {\n      setKeyboardFocused(true);\n    }\n  };\n\n  const handleBlur = (event: FocusEvent<HTMLDivElement>) => {\n    const nextTarget = event.relatedTarget;\n    const staysInside = event.currentTarget.contains(nextTarget);\n    if (!staysInside) {\n      setKeyboardFocused(false);\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (isEditableTarget(event.target)) {\n      return;\n    }\n    const step = stepOfKey(event.key, orientation);\n    if (step === 0) {\n      return;\n    }\n    event.preventDefault();\n    if (step > 0) {\n      next();\n      return;\n    }\n    previous();\n  };\n\n  return (\n    <div\n      role='region'\n      aria-roledescription='carousel'\n      aria-label={ariaLabel}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={() => setHovered(false)}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n      onKeyDown={handleKeyDown}\n      className={cn('group/hover relative', className)}\n    >\n      <div className='overflow-hidden'>{children}</div>\n    </div>\n  );\n}\n\nfunction Carousel({\n  children,\n  className,\n  initialIndex = 0,\n  index: externalIndex,\n  onIndexChange,\n  disableDrag = false,\n  orientation = 'horizontal',\n  loop = false,\n  slidesPerView,\n  autoplay = false,\n  autoplayDelay = 5000,\n  'aria-label': ariaLabel,\n}: CarouselProps) {\n  const [internalIndex, setInternalIndex] = useState<number>(initialIndex);\n  const isControlled = externalIndex !== undefined;\n  const currentIndex = isControlled ? externalIndex : internalIndex;\n\n  const handleIndexChange = (newIndex: number) => {\n    if (!isControlled) {\n      setInternalIndex(newIndex);\n    }\n    onIndexChange?.(newIndex);\n  };\n\n  return (\n    <CarouselProvider\n      initialIndex={currentIndex}\n      onIndexChange={handleIndexChange}\n      disableDrag={disableDrag}\n      orientation={orientation}\n      loop={loop}\n      slidesPerView={slidesPerView}\n      autoplay={autoplay}\n      autoplayDelay={autoplayDelay}\n    >\n      <ReducedMotionConfig>\n        <CarouselRoot className={className} aria-label={ariaLabel}>\n          {children}\n        </CarouselRoot>\n      </ReducedMotionConfig>\n    </CarouselProvider>\n  );\n}\n\nconst NAVIGATION_BUTTON =\n  'bg-card hairline-ring ring-border pointer-events-auto h-fit w-fit rounded-full p-2 shadow-[0_1px_2px_rgb(0_0_0/0.05)] transition-[opacity,scale] duration-150 ease-(--ease-out-strong) active:scale-[0.97]';\n\nconst NAVIGATION_ICON = 'stroke-foreground';\n\nconst NAVIGATION_LAYOUT: Record<CarouselOrientation, string> = {\n  horizontal:\n    'pointer-events-none absolute top-1/2 left-[-12.5%] flex w-[125%] -translate-y-1/2 justify-between px-2',\n  vertical:\n    'pointer-events-none absolute inset-y-2 left-1/2 flex -translate-x-1/2 flex-col items-center justify-between',\n};\n\nfunction navigationVisibilityOf(alwaysShow?: boolean) {\n  if (alwaysShow) {\n    return 'opacity-100 disabled:opacity-40';\n  }\n  return 'opacity-0 group-hover/hover:opacity-100 group-hover/hover:disabled:opacity-40';\n}\n\nfunction indicatorToneOf(isActive: boolean) {\n  if (isActive) {\n    return 'bg-foreground';\n  }\n  return 'bg-foreground/20';\n}\n\nexport type CarouselNavigationProps = {\n  className?: string;\n  classNameButton?: string;\n  alwaysShow?: boolean;\n};\n\nfunction CarouselNavigation({\n  className,\n  classNameButton,\n  alwaysShow,\n}: CarouselNavigationProps) {\n  const { orientation, canGoNext, canGoPrevious, next, previous } =\n    useCarousel();\n  const visibility = navigationVisibilityOf(alwaysShow);\n  const isVertical = orientation === 'vertical';\n\n  return (\n    <div className={cn(NAVIGATION_LAYOUT[orientation], className)}>\n      <button\n        type='button'\n        aria-label='Previous slide'\n        className={cn(NAVIGATION_BUTTON, visibility, classNameButton)}\n        disabled={!canGoPrevious}\n        onClick={previous}\n      >\n        {isVertical && <ChevronUpIcon className={NAVIGATION_ICON} size={16} />}\n        {!isVertical && (\n          <ChevronLeftIcon className={NAVIGATION_ICON} size={16} />\n        )}\n      </button>\n      <button\n        type='button'\n        className={cn(NAVIGATION_BUTTON, visibility, classNameButton)}\n        aria-label='Next slide'\n        disabled={!canGoNext}\n        onClick={next}\n      >\n        {isVertical && (\n          <ChevronDownIcon className={NAVIGATION_ICON} size={16} />\n        )}\n        {!isVertical && (\n          <ChevronRightIcon className={NAVIGATION_ICON} size={16} />\n        )}\n      </button>\n    </div>\n  );\n}\n\nexport type CarouselIndicatorVariant = 'dots' | 'travel';\n\nexport type CarouselIndicatorProps = {\n  className?: string;\n  classNameButton?: string;\n  variant?: CarouselIndicatorVariant;\n};\n\nfunction positionsOf(itemsCount: number, lastIndex: number) {\n  if (itemsCount === 0) {\n    return 0;\n  }\n  return lastIndex + 1;\n}\n\nconst INDICATOR_LAYOUT: Record<CarouselOrientation, string> = {\n  horizontal: 'absolute bottom-0 z-10 flex w-full items-center justify-center',\n  vertical: 'absolute inset-y-0 right-0 z-10 flex items-center justify-center',\n};\n\nconst INDICATOR_ROW: Record<CarouselOrientation, string> = {\n  horizontal: 'flex space-x-2',\n  vertical: 'flex flex-col space-y-2',\n};\n\nfunction CarouselIndicator({\n  className,\n  classNameButton,\n  variant = 'dots',\n}: CarouselIndicatorProps) {\n  const { index, itemsCount, lastIndex, setIndex, orientation } = useCarousel();\n  const { carouselId } = useCarouselInteraction();\n  const positions = positionsOf(itemsCount, lastIndex);\n  const isTravel = variant === 'travel';\n\n  return (\n    <div className={cn(INDICATOR_LAYOUT[orientation], className)}>\n      <div className={INDICATOR_ROW[orientation]}>\n        {Array.from({ length: positions }, (_, i) => {\n          const isActive = index === i;\n          const showsTravellingDot = isTravel && isActive;\n          let tone = indicatorToneOf(isActive);\n          if (isTravel) {\n            tone = indicatorToneOf(false);\n          }\n\n          return (\n            <button\n              key={i}\n              type='button'\n              aria-label={`Go to slide ${i + 1}`}\n              aria-current={isActive}\n              onClick={() => setIndex(i)}\n              className={cn(\n                'relative h-2 w-2 rounded-full transition-opacity duration-300',\n                tone,\n                classNameButton\n              )}\n            >\n              {showsTravellingDot && (\n                <motion.span\n                  layoutId={`carousel-indicator-${carouselId}`}\n                  transition={TRAVEL_TRANSITION}\n                  className='bg-foreground absolute inset-0 rounded-full'\n                />\n              )}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport type CarouselProgressType = 'position' | 'autoplay';\n\nexport type CarouselProgressProps = {\n  className?: string;\n  classNameBar?: string;\n  type?: CarouselProgressType;\n};\n\nconst PROGRESS_TRACK =\n  'bg-foreground/10 relative h-1 w-full overflow-hidden rounded-full';\n\nconst PROGRESS_BAR = 'bg-foreground absolute inset-0 rounded-full';\n\nfunction positionShiftOf(index: number, lastIndex: number) {\n  const positions = lastIndex + 1;\n  const reached = (index + 1) / positions;\n  return (reached - 1) * 100;\n}\n\nfunction progressShiftOf(progress: number) {\n  return `${(progress - 1) * 100}%`;\n}\n\nfunction AutoplayProgress({ className, classNameBar }: CarouselProgressProps) {\n  const { autoplayProgress } = useCarousel();\n  const x = useTransform(autoplayProgress, progressShiftOf);\n\n  return (\n    <div aria-hidden className={cn(PROGRESS_TRACK, className)}>\n      <motion.div style={{ x }} className={cn(PROGRESS_BAR, classNameBar)} />\n    </div>\n  );\n}\n\nfunction CarouselProgress({\n  className,\n  classNameBar,\n  type = 'position',\n}: CarouselProgressProps) {\n  const { index, lastIndex, autoplay } = useCarousel();\n  const isAutoplayType = type === 'autoplay';\n\n  if (isAutoplayType) {\n    if (!autoplay) {\n      return null;\n    }\n    return (\n      <AutoplayProgress className={className} classNameBar={classNameBar} />\n    );\n  }\n\n  const shift = positionShiftOf(index, lastIndex);\n\n  return (\n    <div\n      role='progressbar'\n      aria-label='Slide position'\n      aria-valuemin={1}\n      aria-valuemax={lastIndex + 1}\n      aria-valuenow={index + 1}\n      className={cn(PROGRESS_TRACK, className)}\n    >\n      <div\n        style={{ transform: `translateX(${shift}%)` }}\n        className={cn(\n          PROGRESS_BAR,\n          'transition-transform duration-300 ease-(--ease-out-strong)',\n          classNameBar\n        )}\n      />\n    </div>\n  );\n}\n\nconst SWAP_TRANSITION: Transition = {\n  duration: 0.15,\n  ease: [0.2, 0, 0, 1],\n};\n\nconst SWAP_HIDDEN = { opacity: 0, scale: 0.6, filter: 'blur(2px)' };\n\nconst SWAP_SHOWN = { opacity: 1, scale: 1, filter: 'blur(0px)' };\n\nfunction autoplayLabelOf(isPaused: boolean) {\n  if (isPaused) {\n    return 'Play slideshow';\n  }\n  return 'Pause slideshow';\n}\n\nexport type CarouselAutoplayToggleProps = {\n  className?: string;\n};\n\nfunction CarouselAutoplayToggle({ className }: CarouselAutoplayToggleProps) {\n  const { autoplay, isAutoplayPaused, toggleAutoplay } = useCarousel();\n\n  if (!autoplay) {\n    return null;\n  }\n\n  return (\n    <button\n      type='button'\n      aria-label={autoplayLabelOf(isAutoplayPaused)}\n      onClick={toggleAutoplay}\n      className={cn(\n        'bg-card hairline-ring ring-border text-foreground focus-visible:ring-ring focus-visible:ring-offset-background grid size-8 place-items-center rounded-full shadow-[0_1px_2px_rgb(0_0_0/0.05)] transition-transform duration-150 ease-(--ease-out-strong) outline-none focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.97]',\n        className\n      )}\n    >\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span\n          key={String(isAutoplayPaused)}\n          initial={SWAP_HIDDEN}\n          animate={SWAP_SHOWN}\n          exit={SWAP_HIDDEN}\n          transition={SWAP_TRANSITION}\n          className='grid place-items-center'\n        >\n          {isAutoplayPaused && <PlayIcon className='size-4' />}\n          {!isAutoplayPaused && <PauseIcon className='size-4' />}\n        </motion.span>\n      </AnimatePresence>\n    </button>\n  );\n}\n\nexport type CarouselThumbnailsProps = {\n  children: ReactNode;\n  className?: string;\n  'aria-label'?: string;\n};\n\nconst THUMBNAILS_LAYOUT: Record<CarouselOrientation, string> = {\n  horizontal: 'flex-row overflow-x-auto',\n  vertical: 'flex-col overflow-y-auto',\n};\n\nfunction scrollBehaviorOf(\n  prefersReducedMotion: boolean | null\n): ScrollBehavior {\n  if (prefersReducedMotion) {\n    return 'auto';\n  }\n  return 'smooth';\n}\n\nfunction scrollTargetOf(\n  start: number,\n  size: number,\n  viewStart: number,\n  viewSize: number\n) {\n  const end = start + size;\n  const viewEnd = viewStart + viewSize;\n  if (start < viewStart) {\n    return start;\n  }\n  if (end > viewEnd) {\n    return end - viewSize;\n  }\n  return null;\n}\n\nfunction revealThumbnail(\n  list: HTMLElement,\n  thumbnail: HTMLElement,\n  orientation: CarouselOrientation,\n  behavior: ScrollBehavior\n) {\n  if (orientation === 'vertical') {\n    const top = scrollTargetOf(\n      thumbnail.offsetTop,\n      thumbnail.offsetHeight,\n      list.scrollTop,\n      list.clientHeight\n    );\n    if (top !== null) {\n      list.scrollTo({ top, behavior });\n    }\n    return;\n  }\n  const left = scrollTargetOf(\n    thumbnail.offsetLeft,\n    thumbnail.offsetWidth,\n    list.scrollLeft,\n    list.clientWidth\n  );\n  if (left !== null) {\n    list.scrollTo({ left, behavior });\n  }\n}\n\nfunction CarouselThumbnails({\n  children,\n  className,\n  'aria-label': ariaLabel = 'Slides',\n}: CarouselThumbnailsProps) {\n  const { index, orientation } = useCarousel();\n  const listRef = useRef<HTMLDivElement>(null);\n  const prefersReducedMotion = usePrefersReducedMotion();\n\n  useEffect(() => {\n    const list = listRef.current;\n    if (!list) {\n      return;\n    }\n    const thumbnail = list.querySelector<HTMLElement>(\n      `[data-thumbnail-index=\"${index}\"]`\n    );\n    if (!thumbnail) {\n      return;\n    }\n    const behavior = scrollBehaviorOf(prefersReducedMotion);\n    revealThumbnail(list, thumbnail, orientation, behavior);\n  }, [index, orientation, prefersReducedMotion]);\n\n  return (\n    <motion.div\n      ref={listRef}\n      layoutScroll\n      role='group'\n      aria-label={ariaLabel}\n      className={cn(\n        'relative flex [scrollbar-width:none] gap-2 p-1',\n        THUMBNAILS_LAYOUT[orientation],\n        className\n      )}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type CarouselThumbnailProps = {\n  index: number;\n  children: ReactNode;\n  className?: string;\n  'aria-label'?: string;\n};\n\nfunction CarouselThumbnail({\n  index: thumbnailIndex,\n  children,\n  className,\n  'aria-label': ariaLabel,\n}: CarouselThumbnailProps) {\n  const { index, setIndex } = useCarousel();\n  const { carouselId } = useCarouselInteraction();\n  const isActive = thumbnailIndex === index;\n  const label = ariaLabel ?? `Go to slide ${thumbnailIndex + 1}`;\n\n  return (\n    <button\n      type='button'\n      data-thumbnail-index={thumbnailIndex}\n      data-active={isActive}\n      aria-label={label}\n      aria-current={isActive}\n      onClick={() => setIndex(thumbnailIndex)}\n      className={cn(\n        'focus-visible:ring-ring/40 relative size-14 shrink-0 cursor-pointer overflow-hidden rounded-lg opacity-60 transition-opacity duration-150 outline-none hover:opacity-100 focus-visible:ring-[3px] data-[active=true]:opacity-100',\n        className\n      )}\n    >\n      {children}\n      {isActive && (\n        <motion.span\n          aria-hidden\n          layoutId={`carousel-thumbnail-${carouselId}`}\n          transition={TRAVEL_TRANSITION}\n          className='ring-primary pointer-events-none absolute inset-0 rounded-lg ring-2 ring-inset'\n        />\n      )}\n    </button>\n  );\n}\n\nexport type CarouselContentProps = {\n  children: ReactNode;\n  className?: string;\n  transition?: Transition;\n};\n\nconst DEFAULT_TRANSITION: Transition = {\n  damping: 18,\n  stiffness: 90,\n  type: 'spring',\n  duration: 0.2,\n};\n\nconst DRAG_THRESHOLD = 10;\n\nconst DRAG_CONSTRAINTS = { left: 0, right: 0, top: 0, bottom: 0 };\n\nfunction trackTargetOf(orientation: CarouselOrientation, offset: number) {\n  if (orientation === 'vertical') {\n    return { translateY: `-${offset}%` };\n  }\n  return { translateX: `-${offset}%` };\n}\n\nfunction dragOffsetOf(info: PanInfo, orientation: CarouselOrientation) {\n  if (orientation === 'vertical') {\n    return info.offset.y;\n  }\n  return info.offset.x;\n}\n\nfunction CarouselContent({\n  children,\n  className,\n  transition,\n}: CarouselContentProps) {\n  const {\n    index,\n    setItemsCount,\n    disableDrag,\n    orientation,\n    slidesPerView,\n    lastIndex,\n    next,\n    previous,\n  } = useCarousel();\n  const { setDragging } = useCarouselInteraction();\n  const [visibleItemsCount, setVisibleItemsCount] = useState(1);\n  const dragX = useMotionValue(0);\n  const dragY = useMotionValue(0);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const itemsLength = Children.count(children);\n  const isVertical = orientation === 'vertical';\n  const isDraggable = !disableDrag;\n\n  useEffect(() => {\n    if (!containerRef.current) {\n      return;\n    }\n\n    const options = {\n      root: containerRef.current,\n      threshold: 0.5,\n    };\n\n    const observer = new IntersectionObserver((entries) => {\n      const visibleCount = entries.filter(\n        (entry) => entry.isIntersecting\n      ).length;\n      setVisibleItemsCount(visibleCount);\n    }, options);\n\n    const childNodes = containerRef.current.children;\n    Array.from(childNodes).forEach((child) => observer.observe(child));\n\n    return () => observer.disconnect();\n  }, [children, setItemsCount]);\n\n  useEffect(() => {\n    if (!itemsLength) {\n      return;\n    }\n\n    setItemsCount(itemsLength);\n  }, [itemsLength, setItemsCount]);\n\n  const onDragEnd = (\n    _event: MouseEvent | TouchEvent | globalThis.PointerEvent,\n    info: PanInfo\n  ) => {\n    setDragging(false);\n    const offset = dragOffsetOf(info, orientation);\n\n    if (offset <= -DRAG_THRESHOLD) {\n      next();\n    } else if (offset >= DRAG_THRESHOLD) {\n      previous();\n    }\n  };\n\n  let offset = index * (100 / visibleItemsCount);\n  if (slidesPerView !== undefined) {\n    const containedIndex = Math.min(index, lastIndex);\n    const furthestStart = Math.max(0, itemsLength - slidesPerView);\n    const start = Math.min(containedIndex, furthestStart);\n    offset = start * (100 / slidesPerView);\n  }\n\n  let dragAxis: 'x' | 'y' | false = false;\n  if (isDraggable) {\n    dragAxis = 'x';\n  }\n  if (isDraggable && isVertical) {\n    dragAxis = 'y';\n  }\n\n  let dragStyle = {};\n  if (dragAxis === 'x') {\n    dragStyle = { x: dragX };\n  }\n  if (dragAxis === 'y') {\n    dragStyle = { y: dragY };\n  }\n\n  let dragProps = {};\n  if (isDraggable) {\n    dragProps = {\n      dragConstraints: DRAG_CONSTRAINTS,\n      dragMomentum: false,\n      onDragStart: () => setDragging(true),\n      onDragEnd,\n    };\n  }\n\n  return (\n    <motion.div\n      drag={dragAxis}\n      {...dragProps}\n      style={dragStyle}\n      animate={trackTargetOf(orientation, offset)}\n      transition={transition || DEFAULT_TRANSITION}\n      className={cn(\n        'flex items-center',\n        isVertical && 'flex-col items-stretch',\n        !disableDrag && 'cursor-grab active:cursor-grabbing',\n        className\n      )}\n      ref={containerRef}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type CarouselItemProps = {\n  children: ReactNode;\n  className?: string;\n};\n\nfunction basisOf(slidesPerView: number | undefined) {\n  if (slidesPerView === undefined) {\n    return undefined;\n  }\n  return { flexBasis: `${100 / slidesPerView}%` };\n}\n\nfunction CarouselItem({ children, className }: CarouselItemProps) {\n  const { slidesPerView, orientation } = useCarousel();\n  const isVertical = orientation === 'vertical';\n\n  return (\n    <motion.div\n      style={basisOf(slidesPerView)}\n      className={cn(\n        'w-full min-w-0 shrink-0 grow-0 overflow-hidden',\n        isVertical && 'min-h-0',\n        className\n      )}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport {\n  Carousel,\n  CarouselAutoplayToggle,\n  CarouselContent,\n  CarouselNavigation,\n  CarouselIndicator,\n  CarouselItem,\n  CarouselProgress,\n  CarouselThumbnail,\n  CarouselThumbnails,\n  useCarousel,\n};\n",
      "type": "registry:component",
      "target": "components/core/carousel.tsx"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:component"
}