# Carousel

## Usage

`Carousel` scrolls a row of items sideways. The root is a `region` with `aria-roledescription="carousel"`. Each direct child is one slide, named `Slide N of M`.

`gap` is a spacing step (`0`, `0.5`, `1`, `1.5`, `2`, `3`, or `4`). `padding` uses the same scale and also sets scroll-padding, so a snap point lines up with the content edge. `hasSnap` snaps each child to the start edge. `hasEdgeFade` masks the edge that still has overflow.

`hasButtons` shows previous and next controls when there is something to scroll. A button that cannot move is hidden and disabled. If that button had focus, focus moves to the other one. `hasLoop` wraps from the last item to the first, and the buttons stay available at both ends while the row overflows. Shift plus the wheel scrolls sideways.

The component exposes `scrollNext`, `scrollPrev`, `scrollTo`, `canScrollNext`, and `canScrollPrev` on the template ref.

```vue
<script setup lang="ts">
import { Card } from '@astryx-vue/core/Card'
import { Carousel } from '@astryx-vue/core/Carousel'
</script>

<template>
  <Carousel :gap="2" has-snap label="Feature cards" style="max-width: 520px">
    <Card :width="200" :min-height="100">
      Design System
    </Card>
    <Card :width="200" :min-height="100">
      Documentation
    </Card>
  </Carousel>
</template>
```

### Anatomy

- **Scroll container**: The horizontal overflow area that holds all items.
- **Items**: The default slot, rendered in a row inside the scroll container. With hasSnap, each item snaps to the start edge.
- **Fade edges**: Gradient fades on the left and right edges that indicate more content is available. Enabled by default. Set hasEdgeFade to false to turn it off.
- **Navigation buttons**: Prev/next buttons that appear when the content can scroll in that direction. Enabled by default. Set hasButtons to false to turn it off.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Give the region a `label` that names the set, such as “Feature cards”. |
| Do | Give each card a stable width so one scroll step shows a predictable amount. |
| Do | Turn on `hasSnap` when a reader should land on an item, not between two items. |
| Don't | Put a single short item in a carousel. If it fits, the buttons stay off and the row is just a flex line. |
| Don't | Hide the buttons and the edge fade together when the row overflows. One of them has to show that more content is off to the side. |

## Examples

### Carousel — Cards

A horizontally scrollable row of cards with snap scrolling enabled. Use for feature grids, product lists, or any set of cards that overflows the available width. The carousel adds fade edges and navigation buttons automatically.

```vue
<script setup lang="ts">
import { Card } from '@astryx-vue/core/Card'
import { Carousel } from '@astryx-vue/core/Carousel'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const features = [
  { title: 'Design System', desc: 'Tokens, components, and patterns' },
  { title: 'Documentation', desc: 'API reference and usage guides' },
  { title: 'Sandbox', desc: 'Visual testing and previews' },
  { title: 'Library', desc: 'Component and hook information' },
  { title: 'Contributing', desc: 'Open source development' },
]
</script>

<template>
  <Stack direction="vertical" :gap="3" :max-width="520" style="padding: 8px">
    <Text type="body" weight="bold">
      Browse features
    </Text>
    <Carousel :gap="2" has-snap label="Feature cards">
      <Card v-for="item in features" :key="item.title" :width="200" :min-height="100">
        <Stack direction="vertical" :gap="1">
          <Text type="body" weight="bold">
            {{ item.title }}
          </Text>
          <Text type="supporting" color="secondary">
            {{ item.desc }}
          </Text>
        </Stack>
      </Card>
    </Carousel>
  </Stack>
</template>
```

### Carousel — Snap

Scroll-snap carousel with navigation buttons and team member cards. Each card snaps to the start edge on scroll. Use when items should be viewed one at a time rather than as a continuous strip.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { Badge } from '@astryx-vue/core/Badge'
import { Card } from '@astryx-vue/core/Card'
import { Carousel } from '@astryx-vue/core/Carousel'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const team = [
  { name: 'Alice Chen', role: 'Engineering Lead', color: 'blue' as const },
  { name: 'Bob Smith', role: 'Product Designer', color: 'purple' as const },
  { name: 'Carol Davis', role: 'Product Manager', color: 'green' as const },
  { name: 'Andrew Thomas', role: 'Design Manager', color: 'red' as const },
  { name: 'Gina Wilson', role: 'Software Engineer', color: 'orange' as const },
]
</script>

<template>
  <Stack direction="vertical" :gap="3" :max-width="520" style="padding: 8px">
    <Text type="body" weight="bold">
      Team members
    </Text>
    <Carousel :gap="2" has-snap has-buttons label="Team members">
      <Card v-for="person in team" :key="person.name" :width="180" :min-height="140">
        <Stack direction="vertical" :gap="3" h-align="center">
          <Avatar :name="person.name" size="lg" />
          <Stack direction="vertical" :gap="1" h-align="center">
            <Text type="body" weight="bold">
              {{ person.name }}
            </Text>
            <Badge :variant="person.color">
              {{ person.role }}
            </Badge>
          </Stack>
        </Stack>
      </Card>
    </Carousel>
  </Stack>
</template>
```

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `gap` | `0 \| 0.5 \| 1 \| 1.5 \| 2 \| 3 \| 4` | `1` | Space between items, as a spacing step. |
| `hasButtons` | `boolean` | `true` | Previous and next buttons when the row overflows. |
| `hasEdgeFade` | `boolean` | `true` | Fade the edges that still hide overflow. |
| `hasLoop` | `boolean` | `false` | Wrap from the last item to the first. Buttons stay available at both ends while the row overflows. |
| `hasSnap` | `boolean` | `false` | Snap each direct child to the start edge. |
| `padding` | `SpacingStep` | — | Inline padding inside the scrollport, with matching scroll-padding. |
| `label` | `string` | `Carousel` | Accessible name of the region. The default is localized. |

## Exposed

| Name | Signature | Description |
| --- | --- | --- |
| `scrollNext` | `() => void` | Scroll forward by about one viewport. With `hasLoop`, wraps to the start at the end. |
| `scrollPrev` | `() => void` | Scroll backward by about one viewport. With `hasLoop`, wraps to the end at the start. |
| `scrollTo` | `(index: number) => void` | Scroll the 0-based item to the start edge. The index is clamped. Only the carousel scrolls. |
| `canScrollNext` | `() => boolean` | Whether a forward scroll is available. With `hasLoop`, true whenever the content overflows. |
| `canScrollPrev` | `() => boolean` | Whether a backward scroll is available. With `hasLoop`, true whenever the content overflows. |

## Slots

- `default`: carousel items. Each top-level node is one slide. Fragments are flattened, and empty text is skipped.

## Theme

The root class is `astryx-carousel`. The scrollport is `astryx-carousel-scroller` in the theme layer through `.astryx-carousel__scroller`.

- `--astryx-carousel-gap`: space between items. Set from `gap`.
- `--astryx-carousel-padding`: inline padding and scroll-padding. Set from `padding`.
