{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "Slides on native scroll snapping with no library: swipe, buttons and arrow keys, as a single slide, visible neighbours, a stacked deck or a thumbnail strip.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/carousel.tsx",
      "content": "import type * as React from \"react\"\nimport { useRef } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { CarouselControls, CarouselOverlayButtons } from \"@/registry/miseui/ui/carousel-controls\"\nimport { CarouselSlide, type CarouselItem, type CarouselLabels, type CarouselVariant } from \"@/registry/miseui/ui/carousel-parts\"\nimport { useCarouselState, type CarouselState } from \"@/registry/miseui/ui/carousel-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype CarouselEffect = \"parallax\"\n\nconst DEFAULT_INTERVAL_MS = 6000\n\nconst DEFAULT_LABELS: CarouselLabels = {\n  previous: \"Previous slide\",\n  next: \"Next slide\",\n  pause: \"Pause slides\",\n  play: \"Play slides\",\n  slide: (position, count, label) => `${position} of ${count}: ${label}`,\n}\n\nconst VIEWPORT_CLASS: Record<CarouselVariant, string> = {\n  plain: \"gap-4\",\n  peek: \"gap-3 px-[8%]\",\n  // Room on the right for the cards waiting behind the current one.\n  cards: \"pr-12\",\n  thumbnails: \"gap-4\",\n}\n\ntype RootAttributes = Omit<React.ComponentProps<\"div\">, \"children\" | \"onKeyDown\">\n\ninterface CarouselProps extends RootAttributes {\n  /** What this carousel holds, read out when someone enters it. */\n  label: string\n  items: readonly CarouselItem[]\n  /** `plain` one slide with a count, `peek` neighbours visible, `cards` a stacked deck, `thumbnails` a picture strip. */\n  variant?: CarouselVariant\n  /** `parallax` moves `media` slightly against the swipe. Ignored by `cards`, which already moves in depth. */\n  effect?: CarouselEffect\n  index?: number\n  defaultIndex?: number\n  onIndexChange?: (index: number) => void\n  /** Wraps past the ends with the buttons and arrow keys. Swiping stops at the ends either way. */\n  loop?: boolean\n  /** Off by default. When on it pauses on hover and focus, offers a pause button and never runs under reduced motion. */\n  autoplay?: boolean\n  interval?: number\n  labels?: Partial<CarouselLabels>\n}\n\ninterface CarouselViewportProps {\n  viewportRef: React.RefObject<HTMLDivElement | null>\n  label: string\n  items: readonly CarouselItem[]\n  variant: CarouselVariant\n  parallax: boolean\n  labels: CarouselLabels\n  state: CarouselState\n}\n\nfunction CarouselViewport({ viewportRef, label, items, variant, parallax, labels, state }: CarouselViewportProps) {\n  return (\n    <div\n      ref={viewportRef}\n      tabIndex={0}\n      data-slot=\"carousel-viewport\"\n      aria-label={label}\n      role=\"group\"\n      className={cn(\n        \"relative flex min-w-0 snap-x snap-mandatory overflow-x-auto overscroll-x-contain outline-none\",\n        \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        VIEWPORT_CLASS[variant],\n      )}\n      onScroll={state.onScroll}\n      onScrollEnd={state.onScrollEnd}\n      onPointerDown={state.release}\n      onWheel={state.release}\n    >\n      {items.map((item, position) => (\n        <CarouselSlide key={item.id} item={item} index={position} count={items.length} variant={variant} parallax={parallax} labels={labels} state={state} />\n      ))}\n    </div>\n  )\n}\n\nfunction Carousel({\n  label, items, variant = \"plain\", effect, index, defaultIndex = 0, onIndexChange, loop = false,\n  autoplay = false, interval = DEFAULT_INTERVAL_MS, labels, className, ...props\n}: CarouselProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const viewportRef = useRef<HTMLDivElement>(null)\n  const reduceMotion = useReducedMotion() ?? false\n  const merged = { ...DEFAULT_LABELS, ...labels }\n  const align = variant === \"peek\" ? \"center\" : \"start\"\n  const state = useCarouselState(\n    { count: items.length, align, loop, autoplay, interval, index, defaultIndex, onIndexChange, reduceMotion },\n    { rootRef, viewportRef },\n  )\n  const current = items[state.active]\n  const parallax = effect === \"parallax\" && variant !== \"cards\" && !reduceMotion\n  return (\n    <div\n      ref={rootRef}\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label={label}\n      data-slot=\"carousel\"\n      data-variant={variant}\n      data-effect={effect}\n      className={cn(\"grid w-full min-w-0 grid-cols-[minmax(0,1fr)] gap-3\", className)}\n      onKeyDown={state.onKeyDown}\n      onPointerEnter={() => state.setHovered(true)}\n      onPointerLeave={() => state.setHovered(false)}\n      // Otherwise pressing Play would leave focus on Play, and focus inside holds the slides still.\n      onFocusCapture={(event) => state.setFocused((event.target as HTMLElement).closest(\"[data-slot='carousel-autoplay']\") === null)}\n      onBlurCapture={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) state.setFocused(false)\n      }}\n      {...props}\n    >\n      <div className=\"relative min-w-0\">\n        <CarouselViewport viewportRef={viewportRef} label={label} items={items} variant={variant} parallax={parallax} labels={merged} state={state} />\n        {variant === \"peek\" ? <CarouselOverlayButtons state={state} labels={merged} /> : null}\n      </div>\n      <CarouselControls variant={variant} items={items} state={state} labels={merged} autoplay={autoplay} />\n      <p className=\"sr-only\" aria-live={state.running ? \"off\" : \"polite\"} aria-atomic=\"true\">\n        {current ? merged.slide(state.active + 1, items.length, current.label) : \"\"}\n      </p>\n    </div>\n  )\n}\n\nexport { Carousel, type CarouselProps, type CarouselItem, type CarouselVariant, type CarouselEffect, type CarouselLabels }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/carousel-controls.tsx",
      "content": "import { useEffect, useRef } from \"react\"\nimport { ChevronLeft, ChevronRight, Pause, Play } from \"lucide-react\"\nimport type { CarouselItem, CarouselLabels, CarouselVariant } from \"@/registry/miseui/ui/carousel-parts\"\nimport type { CarouselState } from \"@/registry/miseui/ui/carousel-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst BUTTON_CLASS = cn(\n  \"flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none pointer-coarse:size-10\",\n  \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-accent hover:text-foreground\",\n  \"focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-disabled:pointer-events-none aria-disabled:opacity-40 [&_svg]:size-4\",\n)\nconst OVERLAY_BUTTON_CLASS = \"material absolute top-1/2 z-10 size-10 -translate-y-1/2 rounded-full border text-foreground shadow-sm hover:bg-background\"\n\ninterface StepButtonProps {\n  amount: 1 | -1\n  label: string\n  state: CarouselState\n  className?: string\n}\n\n/** aria-disabled, not disabled: a focused button that went dead would drop the keyboard user back at the top of the page. */\nfunction StepButton({ amount, label, state, className }: StepButtonProps) {\n  const Icon = amount < 0 ? ChevronLeft : ChevronRight\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      aria-disabled={!state.canStep(amount) || undefined}\n      data-slot={amount < 0 ? \"carousel-previous\" : \"carousel-next\"}\n      className={cn(BUTTON_CLASS, className)}\n      onClick={() => state.step(amount)}\n    >\n      <Icon aria-hidden=\"true\" />\n    </button>\n  )\n}\n\nfunction AutoplayButton({ state, labels }: { state: CarouselState; labels: CarouselLabels }) {\n  const Icon = state.stopped ? Play : Pause\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-autoplay\"\n      aria-label={state.stopped ? labels.play : labels.pause}\n      className={BUTTON_CLASS}\n      onClick={() => state.setStopped(!state.stopped)}\n    >\n      <Icon aria-hidden=\"true\" />\n    </button>\n  )\n}\n\ninterface CarouselThumbsProps {\n  items: readonly CarouselItem[]\n  state: CarouselState\n}\n\nfunction CarouselThumbs({ items, state }: CarouselThumbsProps) {\n  const stripRef = useRef<HTMLUListElement>(null)\n  const { active, goTo } = state\n  useEffect(() => {\n    const strip = stripRef.current\n    const thumb = strip?.querySelector<HTMLElement>(\"[aria-current='true']\")\n    if (!strip || !thumb) return\n    // Not scrollIntoView, which would also scroll the page while autoplay runs off screen.\n    strip.scrollTo({ left: thumb.offsetLeft - (strip.clientWidth - thumb.offsetWidth) / 2 })\n  }, [active])\n  return (\n    <ul ref={stripRef} data-slot=\"carousel-thumbs\" className=\"relative flex min-w-0 flex-1 gap-2 overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n      {items.map((item, index) => {\n        const current = active === index\n        return (\n          <li key={item.id} className=\"shrink-0\">\n            <button\n              type=\"button\"\n              aria-label={item.label}\n              aria-current={current ? \"true\" : undefined}\n              className={cn(\n                \"relative block size-12 overflow-hidden rounded-md border bg-muted text-[13px] tabular-nums outline-none\",\n                \"transition-opacity duration-(--duration-fast) ease-(--ease-mise) focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n                current ? \"ring-2 ring-foreground\" : \"opacity-70 hover:opacity-100\",\n              )}\n              onClick={() => goTo(index)}\n            >\n              {item.thumbnail ?? index + 1}\n            </button>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\ninterface CarouselControlsProps {\n  variant: CarouselVariant\n  items: readonly CarouselItem[]\n  state: CarouselState\n  labels: CarouselLabels\n  autoplay: boolean\n}\n\nfunction CarouselControls({ variant, items, state, labels, autoplay }: CarouselControlsProps) {\n  const previous = <StepButton amount={-1} label={labels.previous} state={state} />\n  const next = <StepButton amount={1} label={labels.next} state={state} />\n  const pause = autoplay ? <AutoplayButton state={state} labels={labels} /> : null\n  const count = (\n    <span data-slot=\"carousel-count\" className=\"text-[13px] text-muted-foreground tabular-nums\">\n      {state.active + 1} / {items.length}\n    </span>\n  )\n  if (variant === \"thumbnails\") {\n    return <div className=\"flex items-center gap-1\">{previous}<CarouselThumbs items={items} state={state} />{next}{pause}</div>\n  }\n  if (variant === \"peek\") return pause ? <div className=\"flex justify-end\">{pause}</div> : null\n  if (variant === \"cards\") {\n    return <div className=\"flex items-center justify-center gap-2\">{previous}{count}{next}{pause}</div>\n  }\n  return (\n    <div className=\"flex items-center gap-2\">\n      <span className=\"mr-auto min-w-0 text-[13px] break-words text-muted-foreground\">{items[state.active]?.label}</span>\n      {count}\n      <span className=\"flex\">{previous}{next}{pause}</span>\n    </div>\n  )\n}\n\n/** Laid over the slides, because in `peek` the neighbours already show there is more and a control row would add weight. */\nfunction CarouselOverlayButtons({ state, labels }: { state: CarouselState; labels: CarouselLabels }) {\n  return (\n    <>\n      <StepButton amount={-1} label={labels.previous} state={state} className={cn(OVERLAY_BUTTON_CLASS, \"left-2\")} />\n      <StepButton amount={1} label={labels.next} state={state} className={cn(OVERLAY_BUTTON_CLASS, \"right-2\")} />\n    </>\n  )\n}\n\nexport { CarouselControls, CarouselOverlayButtons }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/carousel-parts.tsx",
      "content": "import type * as React from \"react\"\nimport type { CarouselState } from \"@/registry/miseui/ui/carousel-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype CarouselVariant = \"plain\" | \"peek\" | \"cards\" | \"thumbnails\"\n\ninterface CarouselItem {\n  id: string\n  /** Names the slide for assistive tech and in the count. */\n  label: string\n  content?: React.ReactNode\n  /** Fills the slide at 16:9, behind `content`. The layer `parallax` moves. */\n  media?: React.ReactNode\n  /** Small picture for the `thumbnails` strip. */\n  thumbnail?: React.ReactNode\n}\n\ninterface CarouselLabels {\n  previous: string\n  next: string\n  pause: string\n  play: string\n  slide: (position: number, count: number, label: string) => string\n}\n\n/** Offsets are unitless slides; each variant's distance from the current position drives its transform. */\nconst STACK_STYLE = {\n  \"--carousel-depth\": \"max(0, calc(var(--carousel-index) - var(--carousel-position, 0)))\",\n  transform:\n    \"translateX(calc(var(--carousel-depth) * -100% + min(var(--carousel-depth), 2) * 16px)) scale(calc(1 - min(var(--carousel-depth), 2) * 0.04))\",\n  opacity: \"clamp(0, calc(3 - var(--carousel-depth)), 1)\",\n} as React.CSSProperties\n\nconst PARALLAX_STYLE = {\n  transform: \"translateX(calc(clamp(-1, var(--carousel-position, 0) - var(--carousel-index), 1) * 6%))\",\n} as React.CSSProperties\n\nconst SLIDE_CLASS: Record<CarouselVariant, string> = {\n  plain: \"w-full snap-start\",\n  peek: \"w-full snap-center\",\n  // Snap box stays untransformed: browsers read snap positions from the transformed box, so the frame inside does the moving.\n  cards: \"relative w-full snap-start\",\n  thumbnails: \"w-full snap-start\",\n}\n\nconst FRAME_CLASS: Record<CarouselVariant, string> = {\n  plain: \"rounded-lg border bg-card\",\n  peek: \"rounded-lg border bg-card\",\n  cards: \"origin-right rounded-xl border bg-card shadow-sm will-change-transform\",\n  thumbnails: \"rounded-lg border bg-card\",\n}\n\ninterface CarouselSlideProps {\n  item: CarouselItem\n  index: number\n  count: number\n  variant: CarouselVariant\n  parallax: boolean\n  labels: CarouselLabels\n  state: CarouselState\n}\n\nfunction CarouselSlide({ item, index, count, variant, parallax, labels, state }: CarouselSlideProps) {\n  const current = state.active === index\n  const jumpOnPress = variant === \"peek\" || variant === \"cards\"\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      aria-label={labels.slide(index + 1, count, item.label)}\n      data-slot=\"carousel-slide\"\n      data-active={current ? \"true\" : undefined}\n      style={{ \"--carousel-index\": index, zIndex: variant === \"cards\" ? count - index : undefined } as React.CSSProperties}\n      className={cn(\"min-w-0 shrink-0\", SLIDE_CLASS[variant])}\n      onClickCapture={(event) => {\n        if (current || !jumpOnPress) return\n        // A visible neighbour is a way to get there, not a live control yet.\n        event.preventDefault()\n        event.stopPropagation()\n        state.goTo(index)\n      }}\n    >\n      <div\n        data-slot=\"carousel-frame\"\n        style={variant === \"cards\" ? STACK_STYLE : undefined}\n        className={cn(\"relative h-full overflow-hidden\", FRAME_CLASS[variant], item.media ? \"aspect-video\" : \"\")}\n      >\n        {item.media ? (\n          <div data-slot=\"carousel-media\" style={parallax ? PARALLAX_STYLE : undefined} className={cn(\"absolute inset-0\", parallax && \"-inset-x-[8%]\")}>\n            {item.media}\n          </div>\n        ) : null}\n        {item.content ? <div className=\"relative h-full min-w-0\">{item.content}</div> : null}\n      </div>\n    </div>\n  )\n}\n\nexport { CarouselSlide, type CarouselItem, type CarouselLabels, type CarouselVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/carousel-state.ts",
      "content": "import type * as React from \"react\"\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\"\n\ntype SnapAlign = \"start\" | \"center\"\n\nconst POSITION_VAR = \"--carousel-position\"\nconst ARRIVED_TOLERANCE = 0.01\n\n/** The scrollLeft at which a slide sits snapped. Clamped, because the last slides may not be able to scroll that far. */\nfunction snapStop(left: number, width: number, viewWidth: number, maxScroll: number, align: SnapAlign) {\n  const raw = align === \"center\" ? left - (viewWidth - width) / 2 : left\n  return Math.min(Math.max(raw, 0), Math.max(maxScroll, 0))\n}\n\n/** Fractional slide position for a scroll offset: 1.5 is halfway from the second slide to the third. */\nfunction scrollPosition(stops: readonly number[], scrollLeft: number) {\n  const next = stops.findIndex((stop) => stop > scrollLeft)\n  if (next === -1) return Math.max(stops.length - 1, 0)\n  if (next === 0) return 0\n  const from = stops[next - 1]\n  return next - 1 + (scrollLeft - from) / (stops[next] - from)\n}\n\nfunction readStops(viewport: HTMLElement, align: SnapAlign) {\n  const slides = viewport.querySelectorAll<HTMLElement>(\"[data-slot='carousel-slide']\")\n  const maxScroll = viewport.scrollWidth - viewport.clientWidth\n  return Array.from(slides, (slide) => snapStop(slide.offsetLeft, slide.offsetWidth, viewport.clientWidth, maxScroll, align))\n}\n\nfunction isTextEntry(target: EventTarget) {\n  return target instanceof HTMLElement && target.closest(\"input, textarea, select, [contenteditable='true']\") !== null\n}\n\ninterface CarouselStateOptions {\n  count: number\n  align: SnapAlign\n  loop: boolean\n  autoplay: boolean\n  interval: number\n  index?: number\n  defaultIndex: number\n  onIndexChange?: (index: number) => void\n  reduceMotion: boolean\n}\n\ninterface CarouselRefs {\n  rootRef: React.RefObject<HTMLDivElement | null>\n  viewportRef: React.RefObject<HTMLDivElement | null>\n}\n\nfunction useActiveSlide({ count, index, defaultIndex, onIndexChange }: CarouselStateOptions) {\n  const [uncontrolled, setUncontrolled] = useState(defaultIndex)\n  const active = Math.min(Math.max(index ?? uncontrolled, 0), Math.max(count - 1, 0))\n  const emitted = useRef(active)\n  const onChange = useRef(onIndexChange)\n  useEffect(() => {\n    onChange.current = onIndexChange\n  })\n  const setActive = useCallback((next: number) => {\n    if (emitted.current === next) return\n    emitted.current = next\n    setUncontrolled(next)\n    onChange.current?.(next)\n  }, [])\n  /** Takes note of a value the owner set. True when it is news and the scroller still has to catch up. */\n  const acknowledge = useCallback((next: number) => {\n    if (emitted.current === next) return false\n    emitted.current = next\n    return true\n  }, [])\n  return { active, setActive, acknowledge }\n}\n\nfunction useCarouselPosition(options: CarouselStateOptions, { rootRef, viewportRef }: CarouselRefs) {\n  const { align, reduceMotion } = options\n  const { active, setActive, acknowledge } = useActiveSlide(options)\n  const initial = useRef(active)\n  // While a button-driven scroll is on its way, the slides it passes must not count as the current one.\n  const target = useRef<number | null>(null)\n  const scrollToSlide = useCallback((next: number, behavior: ScrollBehavior) => {\n    const viewport = viewportRef.current\n    if (!viewport) return\n    target.current = next\n    viewport.scrollTo({ left: readStops(viewport, align)[next] ?? 0, behavior })\n  }, [align, viewportRef])\n  const goTo = useCallback((next: number) => {\n    setActive(next)\n    scrollToSlide(next, reduceMotion ? \"auto\" : \"smooth\")\n  }, [reduceMotion, scrollToSlide, setActive])\n  const sync = useCallback(() => {\n    const viewport = viewportRef.current\n    if (!viewport) return\n    const position = scrollPosition(readStops(viewport, align), viewport.scrollLeft)\n    // Written straight to the DOM: scroll fires every frame and only transforms depend on this value.\n    rootRef.current?.style.setProperty(POSITION_VAR, String(position))\n    const nearest = Math.round(position)\n    if (target.current !== null) {\n      const arrived = nearest === target.current && Math.abs(position - nearest) < ARRIVED_TOLERANCE\n      if (!arrived) return\n      target.current = null\n    }\n    setActive(nearest)\n  }, [align, rootRef, setActive, viewportRef])\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current\n    if (viewport && initial.current > 0) viewport.scrollLeft = readStops(viewport, align)[initial.current] ?? 0\n  }, [align, viewportRef])\n  useEffect(() => {\n    if (acknowledge(active)) scrollToSlide(active, reduceMotion ? \"auto\" : \"smooth\")\n  }, [acknowledge, active, reduceMotion, scrollToSlide])\n  // A finger or a wheel takes over from whatever a button started.\n  const release = () => {\n    target.current = null\n  }\n  const onScrollEnd = () => {\n    release()\n    sync()\n  }\n  return { active, goTo, sync, release, onScrollEnd }\n}\n\nfunction useCarouselAutoplay(options: CarouselStateOptions, active: number, goTo: (index: number) => void) {\n  const { autoplay, reduceMotion, count, interval } = options\n  const [hovered, setHovered] = useState(false)\n  const [focused, setFocused] = useState(false)\n  const [stopped, setStopped] = useState(false)\n  const running = autoplay && !reduceMotion && !hovered && !focused && !stopped && count > 1\n  useEffect(() => {\n    if (!running) return\n    const timer = setTimeout(() => goTo((active + 1) % count), interval)\n    return () => clearTimeout(timer)\n  }, [running, active, count, interval, goTo])\n  return { running, stopped, setStopped, setHovered, setFocused }\n}\n\nfunction useCarouselState(options: CarouselStateOptions, refs: CarouselRefs) {\n  const { count, loop } = options\n  const { active, goTo, sync, release, onScrollEnd } = useCarouselPosition(options, refs)\n  const autoplay = useCarouselAutoplay(options, active, goTo)\n  const last = Math.max(count - 1, 0)\n  const canStep = (amount: number) => loop || (active + amount >= 0 && active + amount <= last)\n  const step = (amount: number) => {\n    if (canStep(amount)) goTo((active + amount + count) % count)\n  }\n  const keys: Record<string, () => void> = {\n    ArrowLeft: () => step(-1),\n    ArrowRight: () => step(1),\n    Home: () => goTo(0),\n    End: () => goTo(last),\n  }\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const action = keys[event.key]\n    if (!action || isTextEntry(event.target)) return\n    event.preventDefault()\n    action()\n  }\n  return { ...autoplay, active, canStep, step, goTo, onKeyDown, onScroll: sync, onScrollEnd, release }\n}\n\ntype CarouselState = ReturnType<typeof useCarouselState>\n\nexport { scrollPosition, snapStop, useCarouselState, type CarouselState, type SnapAlign }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}