{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:ui",
  "title": "Carousel",
  "description": "A Motion-powered carousel with flat, tilt, floor, fan, arc and ribbon staging, responsive angles, inactive blur, drag momentum, keyboard navigation, RTL and reduced-motion support.",
  "dependencies": [
    "motion@^12.43.0"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/icon-button.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/carousel/carousel.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/carousel/carousel.tsx",
      "content": "import {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  version,\n  type CSSProperties,\n  type DragEvent,\n  type ForwardedRef,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  MotionConfig,\n  animate,\n  motion,\n  useMotionValue,\n  type AnimationPlaybackControls,\n  type MotionValue,\n} from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton, type IconButtonProps } from \"../icon-button\";\nimport {\n  clampIndex,\n  clampWithRubberBand,\n  getDragDirection,\n  getSnapTarget,\n  pxToIndexDelta,\n} from \"./carousel-utils\";\n\n// Bundlers replace this development flag; copied browser components do not\n// otherwise need Node's ambient types.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nexport type CarouselStaging =\n  \"flat\" | \"tilt\" | \"floor\" | \"fan\" | \"arc\" | \"ribbon\";\nexport type CarouselIntensity = \"subtle\" | \"standard\" | \"dramatic\";\n\n// Motion owns the root element, so the same drag/animation handler names it\n// binds must be omitted from the forwarded HTML props to avoid type clashes\n// (mirrors the AuroraBackground convention).\ntype MotionBackedEventProps =\n  | \"onAnimationStart\"\n  | \"onAnimationEnd\"\n  | \"onAnimationIteration\"\n  | \"onDrag\"\n  | \"onDragStart\"\n  | \"onDragEnd\"\n  | \"onDragEnter\"\n  | \"onDragLeave\"\n  | \"onDragOver\"\n  | \"onDragExit\";\n\ntype MotionSafeDivProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  MotionBackedEventProps\n>;\n\nexport interface CarouselProps extends MotionSafeDivProps {\n  staging?: CarouselStaging;\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  drag?: boolean;\n  intensity?: CarouselIntensity;\n  /** Inactive-card blur in pixels (0–4). Does not affect the active card. */\n  inactiveBlur?: number;\n  children?: ReactNode;\n}\n\nexport interface CarouselContextValue {\n  offset: MotionValue<number>;\n  count: number;\n  index: number;\n  staging: CarouselStaging;\n  intensity: CarouselIntensity;\n  reducedMotion: boolean;\n  dragEnabled: boolean;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  scrollTo: (index: number) => void;\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  onDragStart: () => void;\n  onDragSettle: (index: number) => void;\n}\n\nexport interface UseCarouselReturn {\n  index: number;\n  count: number;\n  staging: CarouselStaging;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  scrollTo: (index: number) => void;\n}\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\nconst CarouselItemIndexContext = createContext<number>(0);\n\nfunction subscribeToMotionPreference(callback: () => void) {\n  const query = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\");\n  query?.addEventListener(\"change\", callback);\n  return () => query?.removeEventListener(\"change\", callback);\n}\nfunction getMotionPreference() {\n  return (\n    window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false\n  );\n}\nfunction getServerMotionPreference() {\n  return false;\n}\n\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n// The OS reduced-motion preference is only trusted after hydration so the first\n// client render matches the server exactly (mirrors AuroraBackground).\nfunction useHasHydrated() {\n  const [hasHydrated, setHasHydrated] = useState(false);\n  useIsomorphicLayoutEffect(() => {\n    setHasHydrated(true);\n  }, []);\n  return hasHydrated;\n}\n\nfunction useCarouselContext(component: string): CarouselContextValue {\n  const context = useContext(CarouselContext);\n  if (!context) {\n    throw new Error(`${component} must be used within a <Carousel>.`);\n  }\n  return context;\n}\n\nexport function useCarousel(): UseCarouselReturn {\n  const {\n    index,\n    count,\n    staging,\n    canScrollPrev,\n    canScrollNext,\n    scrollPrev,\n    scrollNext,\n    scrollTo,\n  } = useCarouselContext(\"useCarousel\");\n  return {\n    index,\n    count,\n    staging,\n    canScrollPrev,\n    canScrollNext,\n    scrollPrev,\n    scrollNext,\n    scrollTo,\n  };\n}\n\n// The far plateau of the distance-driven transforms and the visible radius that\n// governs inert-marking are both wider for the deeper stagings.\nconst stagingVisibleRadius: Record<CarouselStaging, number> = {\n  flat: 0,\n  tilt: 1,\n  floor: 2,\n  fan: 0,\n  arc: 0,\n  ribbon: 0,\n};\n\n// Rotate/depth multiplier written inline on the root; blur is switched off\n// entirely at the calm end so the floor scene never blurs when subtle.\nconst intensityMultiplier: Record<CarouselIntensity, number> = {\n  subtle: 0.6,\n  standard: 1,\n  dramatic: 1.4,\n};\n\nconst carouselSpring = { type: \"spring\", stiffness: 90, damping: 18 } as const;\nconst dragThreshold = 10;\nconst clickSuppressionMs = 160;\n\nconst carouselRootClasses =\n  \"group/carousel relative isolate w-full text-foreground [container-type:inline-size] outline-none\";\n\nconst carouselViewportClasses =\n  \"relative flex w-full justify-center rounded-[var(--dt-radius-lg)] py-[var(--carousel-viewport-pad-block)] [touch-action:pan-y] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background overflow-x-clip [overflow-y:visible] data-[drag=true]:cursor-grab data-[dragging=true]:cursor-grabbing data-[dragging=true]:select-none\";\n\nconst carouselTrackClasses =\n  \"grid w-full items-end justify-items-center min-h-[var(--carousel-min-height)]\";\n\n// No permanent will-change: promoting every slide to a compositor layer for the\n// carousel's lifetime is a standing GPU-memory cost. The tilt/floor staging\n// rules already include an explicit translateZ() term in each item's transform\n// (the Safari shimmer guard), which composites the 3D scenes when they need it;\n// flat is purely 2D.\nconst carouselItemClasses =\n  \"relative [grid-area:1/1] w-[var(--carousel-card-size)] max-w-full [backface-visibility:hidden] motion-reduce:[transition:opacity_200ms_ease]\";\n\nconst carouselShadowClasses =\n  \"pointer-events-none absolute bottom-0 left-1/2 -z-10 h-[var(--carousel-shadow-height)] w-[var(--carousel-shadow-width)] -translate-x-1/2 translate-y-[var(--carousel-shadow-offset)] rounded-[100%] bg-[var(--carousel-shadow-color)] blur-md\";\n\nconst carouselDotsClasses = \"flex items-center justify-center\";\n\nconst carouselDotClasses =\n  \"group/dot flex size-11 shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\nexport function carouselClassNames({\n  className,\n}: Pick<CarouselProps, \"className\"> = {}) {\n  return cn(carouselRootClasses, className);\n}\n\nexport function carouselViewportClassNames({\n  className,\n}: { className?: string } = {}) {\n  return cn(carouselViewportClasses, className);\n}\n\nexport function carouselTrackClassNames({\n  className,\n}: { className?: string } = {}) {\n  return cn(carouselTrackClasses, className);\n}\n\nexport function carouselItemClassNames({\n  className,\n}: { className?: string } = {}) {\n  return cn(carouselItemClasses, className);\n}\n\nfunction ChevronLeftIcon() {\n  return (\n    <svg\n      className=\"rtl:rotate-180\"\n      aria-hidden=\"true\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n      stroke=\"currentColor\"\n    >\n      <path\n        d=\"M10 3.5 5.5 8l4.5 4.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction ChevronRightIcon() {\n  return (\n    <svg\n      className=\"rtl:rotate-180\"\n      aria-hidden=\"true\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n      stroke=\"currentColor\"\n    >\n      <path\n        d=\"m6 3.5 4.5 4.5L6 12.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction findCarouselContent(children: ReactNode): ReactNode {\n  let found: ReactNode = null;\n  Children.forEach(children, (child) => {\n    if (found) return;\n    if (isValidElement(child) && child.type === CarouselContent) {\n      found = child;\n    }\n  });\n  return found;\n}\n\nfunction countSlides(children: ReactNode): number {\n  const content = findCarouselContent(children);\n  if (!isValidElement(content)) return 0;\n  const contentChildren = (content.props as { children?: ReactNode }).children;\n  return Children.toArray(contentChildren).filter(isValidElement).length;\n}\n\nexport const Carousel = forwardRef(function Carousel(\n  {\n    children,\n    className,\n    staging = \"flat\",\n    index,\n    defaultIndex = 0,\n    onIndexChange,\n    drag = true,\n    intensity = \"standard\",\n    inactiveBlur,\n    style,\n    \"aria-label\": ariaLabel,\n    \"aria-labelledby\": ariaLabelledby,\n    ...props\n  }: CarouselProps,\n  forwardedRef: ForwardedRef<HTMLDivElement>,\n) {\n  const count = useMemo(() => countSlides(children), [children]);\n  const isControlled = index !== undefined;\n  const initialIndex = clampIndex(isControlled ? index : defaultIndex, count);\n\n  const offset = useMotionValue(initialIndex);\n  const [uncontrolledIndex, setUncontrolledIndex] = useState(initialIndex);\n  const activeIndex = clampIndex(\n    isControlled ? index : uncontrolledIndex,\n    count,\n  );\n\n  const prefersReducedMotion = useSyncExternalStore(\n    subscribeToMotionPreference,\n    getMotionPreference,\n    getServerMotionPreference,\n  );\n  const hasHydrated = useHasHydrated();\n  const reducedMotion = hasHydrated && prefersReducedMotion === true;\n\n  const [liveMessage, setLiveMessage] = useState(\"\");\n  const animationRef = useRef<AnimationPlaybackControls | null>(null);\n  const dragEnabled = drag && count > 1;\n\n  // Latest committed index, updated synchronously at commit so a microtask\n  // scheduled from an event handler (which runs after React's sync flush but\n  // before passive effects) can read the post-flush value.\n  const committedIndexRef = useRef(activeIndex);\n  useIsomorphicLayoutEffect(() => {\n    committedIndexRef.current = activeIndex;\n  });\n\n  const animateOffsetTo = useCallback(\n    (target: number) => {\n      const clamped = clampIndex(target, count);\n      animationRef.current?.stop();\n      if (reducedMotion) {\n        offset.jump(clamped);\n        animationRef.current = null;\n        return;\n      }\n      animationRef.current = animate(offset, clamped, carouselSpring);\n    },\n    [count, offset, reducedMotion],\n  );\n\n  // A single effect keeps the resting offset synced to the settled index for\n  // control-driven changes and reduced-motion flips. User gestures update the\n  // index below; this effect performs the actual spring/jump.\n  useEffect(() => {\n    animateOffsetTo(activeIndex);\n    return () => animationRef.current?.stop();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [activeIndex, reducedMotion]);\n\n  const settle = useCallback(\n    (target: number, announce: boolean) => {\n      const clamped = clampIndex(target, count);\n      const changed = clamped !== activeIndex;\n      if (isControlled) {\n        // Controlled: the consumer owns the index. After this event flushes,\n        // spring the offset to whatever index is actually committed — if the\n        // consumer accepted the change this targets the new index (the sync\n        // effect does the same), and if it ignored or clamped the callback the\n        // track still settles back onto the held integer instead of resting at\n        // a fractional drag offset forever.\n        queueMicrotask(() => animateOffsetTo(committedIndexRef.current));\n      } else if (changed) {\n        setUncontrolledIndex(clamped);\n      } else {\n        // Snapped back to the same slide: the effect will not re-run, so pull\n        // the dragged offset back to the integer here.\n        animateOffsetTo(clamped);\n      }\n      if (changed) onIndexChange?.(clamped);\n      if (changed && announce) {\n        setLiveMessage(`Slide ${clamped + 1} of ${count}`);\n      }\n    },\n    [activeIndex, animateOffsetTo, count, isControlled, onIndexChange],\n  );\n\n  const scrollTo = useCallback(\n    (target: number) => settle(target, true),\n    [settle],\n  );\n  const scrollPrev = useCallback(\n    () => scrollTo(activeIndex - 1),\n    [activeIndex, scrollTo],\n  );\n  const scrollNext = useCallback(\n    () => scrollTo(activeIndex + 1),\n    [activeIndex, scrollTo],\n  );\n\n  const onDragStart = useCallback(() => {\n    animationRef.current?.stop();\n  }, []);\n  const onDragSettle = useCallback(\n    (target: number) => settle(target, false),\n    [settle],\n  );\n\n  // Development-only nudge: a carousel without an accessible name is an\n  // unlabeled region. Warn once after mount so SSR output stays clean.\n  const warnedRef = useRef(false);\n  useEffect(() => {\n    if (\n      process.env.NODE_ENV !== \"production\" &&\n      !warnedRef.current &&\n      !ariaLabel &&\n      !ariaLabelledby\n    ) {\n      warnedRef.current = true;\n      console.warn(\n        \"Carousel: provide an accessible name via `aria-label` or `aria-labelledby`.\",\n      );\n    }\n  }, [ariaLabel, ariaLabelledby]);\n\n  const contextValue = useMemo<CarouselContextValue>(\n    () => ({\n      offset,\n      count,\n      index: activeIndex,\n      staging,\n      intensity,\n      reducedMotion,\n      dragEnabled,\n      canScrollPrev: activeIndex > 0,\n      canScrollNext: activeIndex < count - 1,\n      scrollTo,\n      scrollPrev,\n      scrollNext,\n      onDragStart,\n      onDragSettle,\n    }),\n    [\n      offset,\n      count,\n      activeIndex,\n      staging,\n      intensity,\n      reducedMotion,\n      dragEnabled,\n      scrollTo,\n      scrollPrev,\n      scrollNext,\n      onDragStart,\n      onDragSettle,\n    ],\n  );\n\n  const rootStyle = {\n    ...style,\n    \"--carousel-offset\": offset,\n    \"--carousel-intensity\": intensityMultiplier[intensity],\n    \"--carousel-blur-enabled\": intensity === \"subtle\" ? 0 : 1,\n    ...(inactiveBlur !== undefined && Number.isFinite(inactiveBlur)\n      ? {\n          \"--carousel-focus-blur\": `${Math.min(4, Math.max(0, inactiveBlur))}px`,\n        }\n      : {}),\n  } as CSSProperties;\n\n  return (\n    <MotionConfig reducedMotion={reducedMotion ? \"always\" : \"user\"}>\n      <CarouselContext.Provider value={contextValue}>\n        <motion.div\n          {...props}\n          ref={forwardedRef}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          aria-label={ariaLabel}\n          aria-labelledby={ariaLabelledby}\n          data-slot=\"carousel\"\n          data-staging={staging}\n          data-intensity={intensity}\n          data-reduced-motion={reducedMotion ? \"true\" : undefined}\n          className={carouselClassNames({ className })}\n          style={rootStyle}\n        >\n          {children}\n          <div className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n            {liveMessage}\n          </div>\n        </motion.div>\n      </CarouselContext.Provider>\n    </MotionConfig>\n  );\n});\n\nCarousel.displayName = \"Carousel\";\n\ninterface DragSession {\n  didDrag: boolean;\n  pointerId: number;\n  direction: 1 | -1;\n  startClientX: number;\n  startOffset: number;\n  step: number;\n}\n\nexport interface CarouselContentProps extends HTMLAttributes<HTMLDivElement> {\n  children?: ReactNode;\n}\n\nexport const CarouselContent = forwardRef<HTMLDivElement, CarouselContentProps>(\n  function CarouselContent(\n    {\n      children,\n      className,\n      // Consumer handlers are chained ahead of the internal drag/keyboard\n      // logic (consumer first; preventDefault opts out), matching onKeyDown.\n      onClickCapture: onClickCaptureProp,\n      onDragStart: onDragStartProp,\n      onKeyDown: onKeyDownProp,\n      onPointerCancel: onPointerCancelProp,\n      onPointerDown: onPointerDownProp,\n      onPointerMove: onPointerMoveProp,\n      onPointerUp: onPointerUpProp,\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const {\n      offset,\n      count,\n      dragEnabled,\n      onDragStart,\n      onDragSettle,\n      scrollPrev,\n      scrollNext,\n      scrollTo,\n    } = useCarouselContext(\"CarouselContent\");\n    const viewportRef = useRef<HTMLDivElement | null>(null);\n    const probeRef = useRef<HTMLSpanElement>(null);\n    const sessionRef = useRef<DragSession | null>(null);\n    const suppressClickUntilRef = useRef(0);\n    const [isDragging, setIsDragging] = useState(false);\n\n    const measureStep = useCallback((viewport: HTMLElement) => {\n      // A zero-height probe sized to `--carousel-step` resolves the CSS length\n      // (including container-query units) to pixels for the drag math, without\n      // measuring the overlapping stacked cards.\n      const probe = probeRef.current;\n      const probeWidth = probe?.getBoundingClientRect().width ?? 0;\n      if (probeWidth > 0) return probeWidth;\n      const item = viewport.querySelector<HTMLElement>(\n        '[data-slot=\"carousel-item\"]',\n      );\n      return item?.getBoundingClientRect().width || viewport.clientWidth || 1;\n    }, []);\n\n    const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n      onPointerDownProp?.(event);\n      if (event.defaultPrevented) return;\n      if (!dragEnabled || event.button !== 0) return;\n      onDragStart();\n      sessionRef.current = {\n        didDrag: false,\n        pointerId: event.pointerId,\n        direction: getDragDirection(\n          getComputedStyle(event.currentTarget).direction === \"rtl\",\n        ),\n        startClientX: event.clientX,\n        startOffset: offset.get(),\n        step: measureStep(event.currentTarget),\n      };\n    };\n\n    const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n      onPointerMoveProp?.(event);\n      if (event.defaultPrevented) return;\n      const session = sessionRef.current;\n      if (!session || session.pointerId !== event.pointerId) return;\n      const delta = event.clientX - session.startClientX;\n      if (!session.didDrag && Math.abs(delta) < dragThreshold) return;\n      if (!session.didDrag) {\n        session.didDrag = true;\n        event.currentTarget.setPointerCapture?.(event.pointerId);\n        setIsDragging(true);\n      }\n      event.preventDefault();\n      const raw =\n        session.startOffset -\n        pxToIndexDelta(delta, session.step, session.direction);\n      offset.set(clampWithRubberBand(raw, 0, Math.max(0, count - 1)));\n    };\n\n    const endDrag = (event: PointerEvent<HTMLDivElement>) => {\n      if (event.defaultPrevented) return;\n      const session = sessionRef.current;\n      if (!session || session.pointerId !== event.pointerId) return;\n      if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n      }\n      sessionRef.current = null;\n      if (!session.didDrag) return;\n      suppressClickUntilRef.current = Date.now() + clickSuppressionMs;\n      setIsDragging(false);\n      const target = getSnapTarget({\n        offset: offset.get(),\n        velocity: offset.getVelocity(),\n        min: 0,\n        max: Math.max(0, count - 1),\n      });\n      onDragSettle(target);\n    };\n\n    const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {\n      onPointerUpProp?.(event);\n      endDrag(event);\n    };\n\n    const handlePointerCancel = (event: PointerEvent<HTMLDivElement>) => {\n      onPointerCancelProp?.(event);\n      endDrag(event);\n    };\n\n    const handleClickCapture = (event: MouseEvent<HTMLDivElement>) => {\n      onClickCaptureProp?.(event);\n      if (Date.now() > suppressClickUntilRef.current) return;\n      suppressClickUntilRef.current = 0;\n      event.preventDefault();\n      event.stopPropagation();\n    };\n\n    const handleDragStart = (event: DragEvent<HTMLDivElement>) => {\n      onDragStartProp?.(event);\n      event.preventDefault();\n    };\n\n    const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n      onKeyDownProp?.(event);\n      if (event.defaultPrevented) return;\n      // Nested controls own their arrow/Home/End keys (text selection, sliders,\n      // menus). Carousel navigation belongs to the viewport itself.\n      if (event.target !== event.currentTarget) return;\n      const rtl = getComputedStyle(event.currentTarget).direction === \"rtl\";\n      switch (event.key) {\n        case \"ArrowRight\":\n          event.preventDefault();\n          if (rtl) scrollPrev();\n          else scrollNext();\n          break;\n        case \"ArrowLeft\":\n          event.preventDefault();\n          if (rtl) scrollNext();\n          else scrollPrev();\n          break;\n        case \"Home\":\n          event.preventDefault();\n          scrollTo(0);\n          break;\n        case \"End\":\n          event.preventDefault();\n          scrollTo(count - 1);\n          break;\n        default:\n          break;\n      }\n    };\n\n    return (\n      /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/no-noninteractive-tabindex -- This focusable viewport supports keyboard navigation and pointer gestures. */\n      <div\n        {...props}\n        ref={(node) => {\n          viewportRef.current = node;\n          if (typeof forwardedRef === \"function\") forwardedRef(node);\n          else if (forwardedRef) forwardedRef.current = node;\n        }}\n        tabIndex={0}\n        data-slot=\"carousel-viewport\"\n        data-drag={dragEnabled ? \"true\" : undefined}\n        data-dragging={isDragging ? \"true\" : undefined}\n        className={carouselViewportClassNames({ className })}\n        onKeyDown={handleKeyDown}\n        onClickCapture={handleClickCapture}\n        onDragStart={handleDragStart}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerCancel}\n      >\n        {/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/no-noninteractive-tabindex */}\n        <span\n          ref={probeRef}\n          aria-hidden=\"true\"\n          data-slot=\"carousel-step-probe\"\n          className=\"pointer-events-none absolute h-0 w-[var(--carousel-step)]\"\n        />\n        <div data-slot=\"carousel-track\" className={carouselTrackClasses}>\n          {Children.map(children, (child, itemIndex) =>\n            isValidElement(child) ? (\n              <CarouselItemIndexContext.Provider value={itemIndex}>\n                {child}\n              </CarouselItemIndexContext.Provider>\n            ) : (\n              child\n            ),\n          )}\n        </div>\n      </div>\n    );\n  },\n);\n\nCarouselContent.displayName = \"CarouselContent\";\n\nexport interface CarouselItemProps extends HTMLAttributes<HTMLDivElement> {\n  children?: ReactNode;\n}\n\nexport const CarouselItem = forwardRef<HTMLDivElement, CarouselItemProps>(\n  function CarouselItem(\n    { children, className, style, ...props },\n    forwardedRef,\n  ) {\n    const { count, index, staging, scrollTo } =\n      useCarouselContext(\"CarouselItem\");\n    const itemIndex = useContext(CarouselItemIndexContext);\n    const isInert = Math.abs(itemIndex - index) > stagingVisibleRadius[staging];\n    const showShadow = staging !== \"flat\";\n    const itemRef = useRef<HTMLDivElement | null>(null);\n\n    // Two-phase inert: the render that moves a slide out of view keeps the old\n    // inert state, then a pre-paint layout effect rescues focus (the inert\n    // attribute would silently blur a focused descendant to document.body) and\n    // commits the new state. The initializer keeps SSR/first paint correct.\n    const [appliedInert, setAppliedInert] = useState(isInert);\n    useIsomorphicLayoutEffect(() => {\n      if (isInert === appliedInert) return;\n      const node = itemRef.current;\n      if (\n        isInert &&\n        node &&\n        typeof document !== \"undefined\" &&\n        node.contains(document.activeElement)\n      ) {\n        const viewport = node.closest<HTMLElement>(\n          '[data-slot=\"carousel-viewport\"]',\n        );\n        viewport?.focus();\n      }\n      setAppliedInert(isInert);\n    }, [isInert, appliedInert]);\n\n    return (\n      <div\n        {...props}\n        ref={(node) => {\n          itemRef.current = node;\n          if (typeof forwardedRef === \"function\") forwardedRef(node);\n          else if (forwardedRef) forwardedRef.current = node;\n        }}\n        role=\"group\"\n        aria-roledescription=\"slide\"\n        aria-label={`${itemIndex + 1} of ${count}`}\n        data-slot=\"carousel-item\"\n        data-active={itemIndex === index ? \"true\" : undefined}\n        className={carouselItemClassNames({ className })}\n        style={\n          {\n            ...style,\n            \"--carousel-item-index\": itemIndex,\n            \"--carousel-item-polarity\": itemIndex % 2 === 0 ? 1 : -1,\n          } as CSSProperties\n        }\n      >\n        {showShadow ? (\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"carousel-item-shadow\"\n            className={carouselShadowClasses}\n          />\n        ) : null}\n        <div\n          data-slot=\"carousel-item-content\"\n          className=\"contents\"\n          // Keep the preview selector outside inert content. React 18 needs\n          // the string form of this native attribute; React 19 uses a boolean.\n          {...({\n            inert: appliedInert\n              ? version.startsWith(\"18.\")\n                ? \"\"\n                : true\n              : undefined,\n          } as unknown as HTMLAttributes<HTMLDivElement>)}\n        >\n          {children}\n        </div>\n        {itemIndex !== index ? (\n          <button\n            type=\"button\"\n            data-slot=\"carousel-preview\"\n            aria-label={`Show slide ${itemIndex + 1}`}\n            tabIndex={-1}\n            className=\"absolute inset-0 z-10 cursor-pointer rounded-[inherit]\"\n            onClick={(event) => {\n              if (event.defaultPrevented) return;\n              event.stopPropagation();\n              if (document.activeElement === event.currentTarget) {\n                itemRef.current\n                  ?.closest<HTMLElement>('[data-slot=\"carousel-viewport\"]')\n                  ?.focus();\n              }\n              scrollTo(itemIndex);\n            }}\n          />\n        ) : null}\n      </div>\n    );\n  },\n);\n\nCarouselItem.displayName = \"CarouselItem\";\n\nexport interface CarouselControlProps extends Omit<\n  IconButtonProps,\n  \"aria-label\" | \"aria-labelledby\" | \"children\"\n> {\n  label?: string;\n}\n\nexport const CarouselPrevious = forwardRef<\n  HTMLButtonElement,\n  CarouselControlProps\n>(function CarouselPrevious(\n  {\n    label = \"Previous slide\",\n    onClick,\n    disabled,\n    size = \"sm\",\n    variant = \"outline\",\n    ...props\n  },\n  forwardedRef,\n) {\n  const { canScrollPrev, scrollPrev } = useCarouselContext(\"CarouselPrevious\");\n  return (\n    <IconButton\n      {...props}\n      ref={forwardedRef}\n      aria-label={label}\n      data-slot=\"carousel-previous\"\n      size={size}\n      variant={variant}\n      disabled={disabled ?? !canScrollPrev}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) scrollPrev();\n      }}\n    >\n      <ChevronLeftIcon />\n    </IconButton>\n  );\n});\n\nCarouselPrevious.displayName = \"CarouselPrevious\";\n\nexport const CarouselNext = forwardRef<HTMLButtonElement, CarouselControlProps>(\n  function CarouselNext(\n    {\n      label = \"Next slide\",\n      onClick,\n      disabled,\n      size = \"sm\",\n      variant = \"outline\",\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const { canScrollNext, scrollNext } = useCarouselContext(\"CarouselNext\");\n    return (\n      <IconButton\n        {...props}\n        ref={forwardedRef}\n        aria-label={label}\n        data-slot=\"carousel-next\"\n        size={size}\n        variant={variant}\n        disabled={disabled ?? !canScrollNext}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) scrollNext();\n        }}\n      >\n        <ChevronRightIcon />\n      </IconButton>\n    );\n  },\n);\n\nCarouselNext.displayName = \"CarouselNext\";\n\nexport interface CarouselDotsProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> {\n  label?: (index: number) => string;\n}\n\nexport const CarouselDots = forwardRef<HTMLDivElement, CarouselDotsProps>(\n  function CarouselDots(\n    { className, label = (i) => `Go to slide ${i + 1}`, ...props },\n    forwardedRef,\n  ) {\n    const { count, index, scrollTo } = useCarouselContext(\"CarouselDots\");\n    return (\n      <div\n        {...props}\n        ref={forwardedRef}\n        data-slot=\"carousel-dots\"\n        className={cn(carouselDotsClasses, className)}\n      >\n        {Array.from({ length: count }, (_, i) => (\n          <button\n            key={i}\n            type=\"button\"\n            data-slot=\"carousel-dot\"\n            aria-label={label(i)}\n            aria-current={i === index ? \"true\" : undefined}\n            className={carouselDotClasses}\n            onClick={() => scrollTo(i)}\n          >\n            <span\n              aria-hidden=\"true\"\n              className=\"bg-muted-foreground/30 group-hover/dot:bg-muted-foreground/60 group-aria-[current=true]/dot:bg-primary size-2 rounded-full transition-[background-color,transform] group-aria-[current=true]/dot:scale-125 motion-reduce:transition-none forced-colors:border forced-colors:border-[ButtonText] forced-colors:group-aria-[current=true]/dot:bg-[Highlight]\"\n            />\n          </button>\n        ))}\n      </div>\n    );\n  },\n);\n\nCarouselDots.displayName = \"CarouselDots\";\n"
    },
    {
      "path": "packages/components/src/components/carousel/carousel-utils.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/carousel/carousel-utils.ts",
      "content": "// Pure math for the Carousel offset engine. Everything here is framework-free\n// and deterministic so it can be tested in isolation and reused by the drag,\n// button, keyboard, and controlled-sync paths without touching the DOM.\n\nexport type CarouselDragDirection = 1 | -1;\n\n/**\n * Center-to-center distance between two adjacent slides, in pixels. Falls back\n * to a safe positive number so a zero-width measurement (SSR, hidden, or\n * pre-layout) can never divide the drag math by zero.\n */\nexport function getSlideStep(itemWidth: number, gap: number): number {\n  const step = itemWidth + gap;\n  return Number.isFinite(step) && step > 0 ? step : 1;\n}\n\n/**\n * Convert a pointer displacement in pixels into a delta in float index units.\n * `direction` carries the writing direction (1 for LTR, -1 for RTL) so a\n * right-drag always advances toward higher indices visually.\n */\nexport function pxToIndexDelta(\n  px: number,\n  step: number,\n  direction: CarouselDragDirection,\n): number {\n  return (px / getSlideStep(step, 0)) * direction;\n}\n\n/** Drag sign for the current writing direction. */\nexport function getDragDirection(rtl: boolean): CarouselDragDirection {\n  return rtl ? -1 : 1;\n}\n\n/** Clamp an index into `[0, count - 1]`, collapsing an empty carousel to 0. */\nexport function clampIndex(index: number, count: number): number {\n  if (count <= 0) return 0;\n  if (index < 0) return 0;\n  if (index > count - 1) return count - 1;\n  return index;\n}\n\n/**\n * Rubber-band a value that has been dragged past its bounds. Inside the range\n * the value is returned untouched; beyond it the overshoot is scaled down by\n * `resistance` so the ends feel elastic rather than hard.\n */\nexport function clampWithRubberBand(\n  value: number,\n  min: number,\n  max: number,\n  resistance = 0.35,\n): number {\n  if (min > max) return min;\n  if (value < min) return min - (min - value) * resistance;\n  if (value > max) return max + (value - max) * resistance;\n  return value;\n}\n\nexport interface SnapTargetOptions {\n  /** Current continuous offset in index units. */\n  offset: number;\n  /** Release velocity in index units per second. */\n  velocity: number;\n  /** Lowest selectable index (usually 0). */\n  min: number;\n  /** Highest selectable index (usually count - 1). */\n  max: number;\n  /** Seconds of velocity to project forward before snapping. */\n  projection?: number;\n}\n\n/**\n * Project the release velocity forward, then snap to the nearest whole slide\n * and clamp into range. With zero velocity this is just \"round to the nearest\n * slide\"; a flick biases the result toward the direction of travel.\n */\nexport function getSnapTarget({\n  offset,\n  velocity,\n  min,\n  max,\n  projection = 0.18,\n}: SnapTargetOptions): number {\n  const projected = offset + velocity * projection;\n  const snapped = Math.round(projected);\n  return Math.min(max, Math.max(min, snapped));\n}\n"
    },
    {
      "path": "packages/components/src/components/carousel/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/carousel/index.ts",
      "content": "export {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n  CarouselDots,\n  useCarousel,\n  carouselClassNames,\n  carouselViewportClassNames,\n  carouselTrackClassNames,\n  carouselItemClassNames,\n  type CarouselProps,\n  type CarouselContentProps,\n  type CarouselItemProps,\n  type CarouselControlProps,\n  type CarouselDotsProps,\n  type CarouselContextValue,\n  type CarouselIntensity,\n  type CarouselStaging,\n  type UseCarouselReturn,\n} from \"./carousel\";\nexport {\n  clampIndex,\n  clampWithRubberBand,\n  getDragDirection,\n  getSlideStep,\n  getSnapTarget,\n  pxToIndexDelta,\n  type CarouselDragDirection,\n  type SnapTargetOptions,\n} from \"./carousel-utils\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
