# Carousel

A row of slides that the user moves through with buttons, swipes or arrow keys.

```tsx
import { Card, CardContent } from "@zenoxai/ui/components/card"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@zenoxai/ui/components/carousel"

function CarouselDemo() {
  const threats = [
    { name: "Phishing kits", count: 18 },
    { name: "Lookalike domains", count: 42 },
    { name: "Leaked credentials", count: 7 },
    { name: "Exposed services", count: 23 },
    { name: "Fake social profiles", count: 5 },
  ]

  return (
    <Carousel aria-label="Threats this week" className="w-full max-w-xs">
      <CarouselContent>
        {threats.map((threat, index) => (
          <CarouselItem
            key={threat.name}
            aria-label={`${index + 1} of ${threats.length}`}
          >
            <Card>
              <CardContent className="flex aspect-video flex-col items-center justify-center gap-1">
                <span className="text-xl font-semibold text-text-primary">
                  {threat.count}
                </span>
                <span className="text-sm text-text-secondary">
                  {threat.name}
                </span>
              </CardContent>
            </Card>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}
```

## Usage

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@zenoxai/ui/components/carousel"
```

```tsx
<Carousel aria-label="Threats this week">
  <CarouselContent>
    <CarouselItem aria-label="1 of 3">...</CarouselItem>
    <CarouselItem aria-label="2 of 3">...</CarouselItem>
    <CarouselItem aria-label="3 of 3">...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

Use a carousel when items are browsed one or a few at a time and their order matters little:
evidence screenshots, report templates, onboarding cards. It hides most of its content, so do
not put information there that the user must see. For a list the user scans or compares, use
a grid or a [Scroll Area](/components/scroll-area).

The component wraps [Embla Carousel](https://www.embla-carousel.com). Pass Embla options
through `opts` and plugins (autoplay, wheel gestures) through `plugins`. From the `md`
breakpoint (768px), `CarouselPrevious` and `CarouselNext` sit outside the slides, 48px to each
side: leave that room around the carousel, or position them yourself with `className`. On
smaller screens they move inside the edges, 8px in, over the slides, so they stay on screen
and within reach; change that placement with `max-md:` classes.

## Examples

### Several slides per view

Set the width of each slide with a `basis-*` class on `CarouselItem`. With
`opts={{ align: "start" }}` the slides align to the start of the row instead of the center.

```tsx
import { Card, CardContent } from "@zenoxai/ui/components/card"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@zenoxai/ui/components/carousel"

function CarouselMultiple() {
  const reports = [
    "Weekly exposure",
    "Monthly exposure",
    "Brand abuse",
    "Takedown status",
    "Executive summary",
    "Vulnerability trends",
  ]

  return (
    <Carousel
      aria-label="Report templates"
      opts={{ align: "start" }}
      className="w-full max-w-md"
    >
      <CarouselContent>
        {reports.map((report, index) => (
          <CarouselItem
            key={report}
            aria-label={`${index + 1} of ${reports.length}`}
            className="basis-1/2 sm:basis-1/3"
          >
            <Card>
              <CardContent className="flex aspect-square items-center justify-center text-center text-sm font-medium text-text-primary">
                {report}
              </CardContent>
            </Card>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}
```

### Reading the position

`setApi` hands you the Embla API. Subscribe to its `select` event to show the current slide or
to drive your own controls.

```tsx
import * as React from "react"
import { Card, CardContent } from "@zenoxai/ui/components/card"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@zenoxai/ui/components/carousel"

function CarouselWithApi() {
  const [api, setApi] = React.useState<CarouselApi>()
  const [current, setCurrent] = React.useState(0)
  const captures = [
    "21 Oct, 09:14",
    "21 Oct, 15:40",
    "22 Oct, 08:02",
    "23 Oct, 11:27",
  ]

  React.useEffect(() => {
    if (!api) return
    const onSelect = () => setCurrent(api.selectedScrollSnap())
    api.on("select", onSelect)
    return () => {
      api.off("select", onSelect)
    }
  }, [api])

  return (
    <div className="flex w-full max-w-xs flex-col items-center gap-2">
      <Carousel
        aria-label="Screenshots of acme-login.co"
        setApi={setApi}
        className="w-full"
      >
        <CarouselContent>
          {captures.map((capture, index) => (
            <CarouselItem
              key={capture}
              aria-label={`${index + 1} of ${captures.length}`}
            >
              <Card>
                <CardContent className="flex aspect-video items-center justify-center text-sm text-text-secondary">
                  Captured {capture}
                </CardContent>
              </Card>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
      <span className="text-sm text-text-muted" aria-live="polite">
        Screenshot {current + 1} of {captures.length}
      </span>
    </div>
  )
}
```

## Accessibility

- `Carousel` is a region with `aria-roledescription="carousel"`, and each `CarouselItem` a
  group with `aria-roledescription="slide"`. Give the carousel an `aria-label` and each slide
  a position label such as "2 of 5".
- Left and Right arrow keys move between slides while focus is inside the carousel. The
  buttons are disabled at either end, unless `opts={{ loop: true }}`.
- The buttons carry the English labels "Previous slide" and "Next slide". Pass a translated
  `aria-label` when the interface is not in English.
- Do not autoplay. If a plugin does, give the user a control to pause it.

## API reference

`import { ... } from "@zenoxai/ui/components/carousel"`

### Carousel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| opts | `Partial<OptionsType>` |  | Embla options, such as `loop` or `align`; `axis` follows `orientation`. |
| orientation | `"horizontal" \| "vertical"` | `horizontal` | Scroll direction. Defaults to horizontal. |
| plugins | `CreatePluginType<LoosePluginType, {}>[]` |  | Embla plugins, such as autoplay. |
| setApi | `((api: EmblaCarouselType) => void)` |  | Receives the Embla API when ready, to drive the carousel from outside. |

### CarouselContent

No props of its own.

### CarouselItem

No props of its own.

### CarouselPrevious

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| focusableWhenDisabled | `boolean` | `false` | Whether the button should be focusable when disabled. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, ButtonState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| size | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` | `icon-sm` | Height: `xs` 24px, `sm` 28px, `default` 32px, `lg` 36px. The `icon-*` sizes are square buttons of the same heights for a single icon. The icon follows the size name: 12px at `xs`, 14px at `sm`, 16px at `default` and `lg`. |
| variant | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `outline` | Visual weight. `default` is the filled primary action, one per view; `outline` and `secondary` are for the other actions; `ghost` has no fill until hover, for toolbars and icon buttons; `destructive` is for delete and other irreversible actions; `link` looks like inline text. |

### CarouselNext

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| focusableWhenDisabled | `boolean` | `false` | Whether the button should be focusable when disabled. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, ButtonState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| size | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` | `icon-sm` | Height: `xs` 24px, `sm` 28px, `default` 32px, `lg` 36px. The `icon-*` sizes are square buttons of the same heights for a single icon. The icon follows the size name: 12px at `xs`, 14px at `sm`, 16px at `default` and `lg`. |
| variant | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `outline` | Visual weight. `default` is the filled primary action, one per view; `outline` and `secondary` are for the other actions; `ghost` has no fill until hover, for toolbars and icon buttons; `destructive` is for delete and other irreversible actions; `link` looks like inline text. |

### useCarousel

No props of its own.

