# Carousel

A carousel with motion, drag gestures, keyboard navigation, and Embla-powered slide state.

Use Carousel for image galleries, card reels, onboarding panels, and compact item pickers where adjacent content should be discoverable without leaving the current view.

## Import

```ts
import {
  CarouselComponent,
  CarouselContentComponent,
  CarouselItemComponent,
  CarouselNextComponent,
  CarouselPreviousComponent,
  type CarouselApi,
  type CarouselOptions,
} from '@edsis/component/carousel';
```

## Usage

Compose the carousel from a root, content track, items, and optional previous/next buttons.

```html
<Carousel class="w-full max-w-xs">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <button CarouselPrevious></button>
  <button CarouselNext></button>
</Carousel>
```

## Composition

```text
Carousel
├── CarouselContent
│   ├── CarouselItem
│   └── CarouselItem
├── button[CarouselPrevious]
└── button[CarouselNext]
```

## Common patterns

### Card slides

```html
<Carousel class="mx-auto w-full max-w-xs">
  <CarouselContent>
    @for (slide of slides; track slide.id) {
    <CarouselItem [ariaLabel]="slide.label">
      <div class="p-1">
        <Card>
          <CardContent class="flex aspect-square items-center justify-center p-6">
            <span class="text-4xl font-semibold">{{ slide.value }}</span>
          </CardContent>
        </Card>
      </div>
    </CarouselItem>
    }
  </CarouselContent>
  <button CarouselPrevious></button>
  <button CarouselNext></button>
</Carousel>
```

### Sizes

Use basis utilities on each `CarouselItem` when multiple slides should be partially visible.

```ts
readonly startOptions: CarouselOptions = { align: 'start' };
```

```html
<Carousel [opts]="startOptions" class="w-full max-w-sm">
  <CarouselContent>
    <CarouselItem class="basis-1/2 lg:basis-1/3">...</CarouselItem>
    <CarouselItem class="basis-1/2 lg:basis-1/3">...</CarouselItem>
    <CarouselItem class="basis-1/2 lg:basis-1/3">...</CarouselItem>
  </CarouselContent>
</Carousel>
```

### Spacing

Match shadcn spacing by pairing a negative margin on `CarouselContent` with padding on `CarouselItem`.

```html
<Carousel class="w-full max-w-sm">
  <CarouselContent class="-ml-2 md:-ml-4">
    <CarouselItem class="basis-1/2 pl-2 md:pl-4 lg:basis-1/3">...</CarouselItem>
    <CarouselItem class="basis-1/2 pl-2 md:pl-4 lg:basis-1/3">...</CarouselItem>
    <CarouselItem class="basis-1/2 pl-2 md:pl-4 lg:basis-1/3">...</CarouselItem>
  </CarouselContent>
</Carousel>
```

### Vertical orientation

Set `orientation="vertical"` and give the content viewport a height so Embla can measure the track.

```ts
readonly verticalOptions: CarouselOptions = { align: 'start' };
```

```html
<Carousel orientation="vertical" [opts]="verticalOptions" class="w-full max-w-xs">
  <CarouselContent class="-mt-1 h-67.5">
    <CarouselItem class="basis-1/2 pt-1">...</CarouselItem>
    <CarouselItem class="basis-1/2 pt-1">...</CarouselItem>
    <CarouselItem class="basis-1/2 pt-1">...</CarouselItem>
  </CarouselContent>
</Carousel>
```

### Options

Pass Embla options through `[opts]`. Keep the object as a class property so Angular does not create a new object every change detection pass.

```ts
readonly loopOptions: CarouselOptions = {
  align: 'start',
  loop: true,
};
```

```html
<Carousel [opts]="loopOptions">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>
```

### API and events

The root component exposes `selectedIndex`, `slideCount`, `canScrollPrev`, and `canScrollNext` as signals. Use a template reference for simple status UI.

```html
<Carousel #carouselRef class="w-full max-w-xs">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

<p>Slide {{ carouselRef.selectedIndex() + 1 }} of {{ carouselRef.slideCount() }}</p>
```

Use `(apiReady)` when you need the lower-level Embla API.

```ts
connectCarousel(api: CarouselApi): void {
  api.on('select', () => {
    this.currentSlide.set(api.selectedScrollSnap() + 1);
  });
}
```

```html
<Carousel (apiReady)="connectCarousel($event)"> ... </Carousel>
```

### Plugins

Optional Embla plugins are passed through `[plugins]`. Install plugin packages separately.

```ts
import Autoplay from 'embla-carousel-autoplay';

readonly plugins = [Autoplay({ delay: 2000 })];
```

```html
<Carousel [plugins]="plugins">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>
```

### RTL

Set both `dir="rtl"` and `opts.direction` so layout direction and Embla motion agree.

```ts
readonly rtlOptions: CarouselOptions = { direction: 'rtl' };
```

```html
<section dir="rtl">
  <Carousel dir="rtl" [opts]="rtlOptions" class="w-full max-w-xs">
    <CarouselContent>
      <CarouselItem>...</CarouselItem>
      <CarouselItem>...</CarouselItem>
      <CarouselItem>...</CarouselItem>
    </CarouselContent>
    <button CarouselPrevious class="rtl:rotate-180"></button>
    <button CarouselNext class="rtl:rotate-180"></button>
  </Carousel>
</section>
```

## API reference

### `CarouselComponent`

| Input or output | Type                         | Default                                        |
| --------------- | ---------------------------- | ---------------------------------------------- |
| `orientation`   | `'horizontal' \| 'vertical'` | `'horizontal'`                                 |
| `opts`          | `CarouselOptions`            | `{}`                                           |
| `plugins`       | `readonly CarouselPlugin[]`  | `[]`                                           |
| `keyboard`      | `boolean`                    | `true`                                         |
| `label`         | `string`                     | `'Carousel'`                                   |
| `class`         | `string`                     | `''`                                           |
| `apiReady`      | `CarouselApi` output         | emitted after Embla initializes                |
| `apiChange`     | `CarouselApi \| null` output | emitted when Embla initializes or is destroyed |

### Root signals

| Signal          | Type              |
| --------------- | ----------------- |
| `selectedIndex` | `Signal<number>`  |
| `slideCount`    | `Signal<number>`  |
| `canScrollPrev` | `Signal<boolean>` |
| `canScrollNext` | `Signal<boolean>` |

### Parts

| Part                       | Inputs               |
| -------------------------- | -------------------- |
| `CarouselContent`          | `class`              |
| `CarouselItem`             | `ariaLabel`, `class` |
| `button[CarouselPrevious]` | `label`, `class`     |
| `button[CarouselNext]`     | `label`, `class`     |

## Styling and theming

The primitive follows the shadcn layout recipe: root is `relative`, content owns the overflow-hidden viewport, and items are `basis-full` by default. Pass `class` to tune width, item basis, spacing, and control placement.

Use theme-aware classes such as `border-border`, `bg-card`, `text-foreground`, `bg-background`, `hover:bg-accent`, and `focus-visible:ring-ring` for custom slide and control styling.

## Accessibility

- The root renders as a labelled `region` with `aria-roledescription="carousel"`.
- Previous and next controls are native buttons with descriptive labels and disabled states.
- Each item renders as a `group` with `aria-roledescription="slide"`; pass `ariaLabel` when the slide needs an explicit spoken label.
- Keep interactive controls inside slides reachable by normal tab order and avoid hiding focus outlines.

## Keyboard interactions

- The root is focusable when `keyboard` is `true`.
- Horizontal carousels use ArrowLeft and ArrowRight to move between slides.
- Vertical carousels use ArrowUp and ArrowDown to move between slides.
- Home moves to the first slide and End moves to the last slide.
- Previous and next buttons also support native Enter and Space activation.

## Angular notes

- This implementation uses vanilla Embla Carousel rather than `embla-carousel-react`.
- Embla initializes only in the browser, so server rendering can produce the static slide markup without touching the DOM API.
- `orientation` controls Embla axis; if `opts.axis` is provided, the Angular `orientation` input wins.
- Define `opts` and `plugins` as class properties to avoid unnecessary Embla reinitialization.

## Source parity

This Angular implementation follows the shadcn Carousel anatomy, options, API, events, plugins, and RTL guidance while translating React props into Angular inputs, outputs, signals, and standalone component imports.
