# Carousel

The slides, one at a time or a few, that a person moves through with buttons, arrow keys or by swiping. It never moves by itself unless you set an `interval`. Under reduced motion or `data-motion="calm"` the slides jump instead of gliding and the interval stops; `data-motion="none"` does the same.

- Registry name: `carousel` (`@candor/carousel`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/carousel

## Install

```bash
npx candor-ui add carousel
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/carousel`.
It brings in: button, locale, motion, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, CarouselStatus, type CarouselTransition } from "@/components/candor/ui/carousel"

const reports = [
  { title: "Weekly deploys", body: "31 deploys, 2 rolled back." },
  { title: "Open incidents", body: "1 open, 4 resolved this week." },
  { title: "Build time", body: "Median 6 min 12 s over the last 100 builds." },
  { title: "Test flakes", body: "3 tests failed and passed on retry." },
]

const transitions: CarouselTransition[] = ["slide", "fade", "vertical", "blur", "zoom", "uncover", "fill"]

// It never moves by itself: add `interval` to auto-advance, and it still stops for the pointer, focus and reduced motion.
// `transition` other than "slide" stacks the slides and draws each change (the drawings are the Pro motion-drawings item's).
export default function Example() {
  const [transition, setTransition] = React.useState<CarouselTransition>("slide")
  return (
    <div className="flex max-w-md flex-col gap-3">
      <label className="flex items-center gap-2 text-xs text-muted-foreground">
        transition
        <select value={transition} onChange={(e) => setTransition(e.target.value as CarouselTransition)} className="rounded-md border bg-background px-2 py-1 text-xs text-foreground">
          {transitions.map((t) => (
            <option key={t}>{t}</option>
          ))}
        </select>
      </label>
      <Carousel key={transition} label="Weekly reports" transition={transition}>
        <CarouselContent>
          {reports.map((r) => (
            <CarouselItem key={r.title}>
              <div className="flex h-32 flex-col justify-center gap-1 rounded-md border bg-card px-16">
                <h3 className="text-sm font-medium">{r.title}</h3>
                <p className="text-sm text-muted-foreground">{r.body}</p>
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
        <div className="mt-2 text-center">
          <CarouselStatus />
        </div>
      </Carousel>
    </div>
  )
}
```

## <Carousel>

Props:

- `interval?: number | undefined`: Milliseconds between automatic advances. Left out, the carousel never moves by itself. When set it still stays still while the pointer is over it or focus is inside it, after the person pauses it, and under reduced motion, `data-motion="calm"` or `data-motion="none"`, and it shows a pause button.
- `transition?: CarouselTransition | undefined` (default `"slide"`): How a change of slide is drawn. `slide` (the default) is a scrolling row the slides glide along. The others stack the slides in one place, show one at a time and draw the change: `fade`, `vertical` (rising from below), `blur`, `zoom`, `uncover` (uncovered from the end of the line) or `fill` (uncovered in four steps). The slides not showing are `inert` and hidden from screen readers. The drawings come with the Pro motion-drawings item; without it a stacked carousel changes slide at once.
- `onIndexChange?: ((index: number) => void) | undefined`: Called with the first visible slide's position (from 0) when it changes, by the buttons, keys, swiping or the interval.
- `label?: string | undefined`: The carousel's accessible name. Defaults to "Carousel".
- `className?: string | undefined`
- `children?: React.ReactNode | undefined`: `CarouselContent` with the slides, and the previous, next and status controls.

## <CarouselContent>

The scrolling row of slides; its direct children are the `CarouselItem`s. It scrolls and snaps natively, so a finger or trackpad moves it exactly as far as the pointer goes. It is a tab stop: Left and Right (mirrored in right-to-left languages) change slide, Home and End go to the ends, while it has focus itself: in a field, a link or a button inside a slide, the keys are that control's.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <CarouselItem>

One slide, labelled "3 of 8" for assistive technology. Give it a width with `basis-*` to show more than one at a time.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <CarouselPrevious>

Goes to the previous slide. At the first slide it stays focusable, is `aria-disabled` and says so.

Props:

- `className?: string | undefined`

## <CarouselNext>

Goes to the next slide. At the last slide it stays focusable, is `aria-disabled` and says so.

Props:

- `className?: string | undefined`

## <CarouselStatus>

"3 of 8": the first visible slide out of all of them, in the locale's digits. Announced politely when it changes.

Props:

- `className?: string | undefined`

## Strings (`carousel`)

The fixed words, English by default. Override any of them under `carousel` in `<LocaleProvider strings={{ carousel: { … } }}>`; the table is exported as `carouselStrings`.

- `carousel`: Carousel
- `slide`: Slide
- `slideOf`: {current} of {total}
- `previous`: Previous slide
- `next`: Next slide
- `atStart`: This is the first slide.
- `atEnd`: This is the last slide.
- `pause`: Pause automatic advance
- `resume`: Resume automatic advance

## Types

### CarouselTransition

How a change of slide is drawn: slides side by side in a scrolling row, or stacked in one place with a drawing.

```ts
"slide" | "fade" | "vertical" | "blur" | "zoom" | "uncover" | "fill"
```

## Motion

- **change** (arrive, rise): In a stacked carousel, the slide the person moved to comes in as its transition draws it (a fade, a rise, a blur, a zoom, or uncovered). Only on a change, never at first draw; a short fade under reduced motion, and at once at the Off level or without the Pro drawings.
- **slide** (move): The slides glide to the one the person chose with the buttons or arrow keys, or to the next one when the interval fires. Swiping moves the row exactly as far as the finger goes, with nothing added. Under reduced motion the slides jump.

Set with `--cd-motion-carousel-<moment>-*` in your brand file; the families are in the design reference.

## Terms used here

- **Calm level**: What Candor's motion does for a person whose operating system asks for reduced motion: the highest-precedence setting, above the app's own motion level (`none`, `calm`, `subtle` or `expressive`) and the brand file. A theme tunes how a change looks, never when it happens, so nothing depends on seeing the motion. (https://candor.themethodic.com/glossary.md#calm-level)

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
