# Pagination

## Overview

A navigation control for moving through pages of a large dataset. Renders Previous/Next buttons, numbered page buttons, and ellipsis for large page counts. Use it below data tables to control which page of records is displayed.

The package also exports a **headless `usePagination` hook** that computes the full page item list (numbers + ellipsis) and exposes navigation helpers — use it when you need full control over the pagination UI.

---

## Exports

| Export                | Description                                    |
| --------------------- | ---------------------------------------------- |
| `Pagination`          | Root nav wrapper                               |
| `PaginationContent`   | Flex container for all items                   |
| `PaginationItem`      | Individual item wrapper                        |
| `PaginationLink`      | Numbered page button                           |
| `PaginationPrevious`  | "Previous" button with left arrow              |
| `PaginationNext`      | "Next" button with right arrow                 |
| `PaginationEllipsis`  | "..." indicator for skipped pages              |
| `usePagination`       | Headless hook — all logic, no UI               |
| `UsePaginationProps`  | TypeScript props type for the hook             |
| `UsePaginationReturn` | TypeScript return type for the hook            |
| `PaginationPageItem`  | Union type for page items returned by the hook |

---

## When to Use

- Below tables with more records than fit on one page
- Any paginated list or grid of content

---

## Anatomy

```
<Pagination>
  <PaginationContent>
    <PaginationItem><PaginationPrevious href="#" /></PaginationItem>
    <PaginationItem><PaginationLink href="#">1</PaginationLink></PaginationItem>
    <PaginationItem><PaginationEllipsis /></PaginationItem>
    <PaginationItem><PaginationLink href="#" isActive>5</PaginationLink></PaginationItem>
    <PaginationItem><PaginationNext href="#" /></PaginationItem>
  </PaginationContent>
</Pagination>
```

---

## Sub-Components

| Component            | Description                       |
| -------------------- | --------------------------------- |
| `Pagination`         | Root nav wrapper                  |
| `PaginationContent`  | Flex container for all items      |
| `PaginationItem`     | Individual item wrapper           |
| `PaginationLink`     | Numbered page button              |
| `PaginationPrevious` | "Previous" button with left arrow |
| `PaginationNext`     | "Next" button with right arrow    |
| `PaginationEllipsis` | "..." indicator for skipped pages |

---

## Props

### PaginationLink

| Prop       | Type         | Description                                                                                                                          |
| ---------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `href`     | `string`     | URL for the page                                                                                                                     |
| `isActive` | `boolean`    | When `true`, renders as the active page                                                                                              |
| `disabled` | `boolean`    | When `true`, renders as non-interactive: removes `href`, sets `aria-disabled`, `tabIndex={-1}`, and `pointer-events-none opacity-50` |
| `onClick`  | `() => void` | Click handler (for controlled pagination)                                                                                            |

### PaginationPrevious / PaginationNext

Both components accept and forward a `disabled` prop to `PaginationLink`. Pass `disabled={!canGoPrev}` / `disabled={!canGoNext}` to disable the controls when at the first or last page — this is the preferred pattern over `aria-disabled` alone.

---

## Examples

### Controlled Pagination

```tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  PaginationEllipsis,
} from 'xertica-ui/ui';
import { useState } from 'react';

const [currentPage, setCurrentPage] = useState(1);
const totalPages = 10;

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious onClick={() => setCurrentPage(p => Math.max(1, p - 1))} />
    </PaginationItem>
    {[1, 2, 3].map(page => (
      <PaginationItem key={page}>
        <PaginationLink isActive={currentPage === page} onClick={() => setCurrentPage(page)}>
          {page}
        </PaginationLink>
      </PaginationItem>
    ))}
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink onClick={() => setCurrentPage(totalPages)}>{totalPages}</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} />
    </PaginationItem>
  </PaginationContent>
</Pagination>;
```

---

## `usePagination` Hook

A headless hook that computes the full page item list (page numbers + ellipsis markers) and exposes navigation helpers. Supports both **uncontrolled** (internal state) and **controlled** (external `page` prop) modes.

### Props

| Prop           | Type                     | Default | Description                                                                                 |
| -------------- | ------------------------ | ------- | ------------------------------------------------------------------------------------------- |
| `totalItems`   | `number`                 | —       | **Required.** Total number of items across all pages                                        |
| `pageSize`     | `number`                 | `10`    | Number of items per page                                                                    |
| `initialPage`  | `number`                 | `1`     | Initial page (uncontrolled mode only)                                                       |
| `page`         | `number`                 | —       | Controlled current page — when provided, the hook uses this value instead of internal state |
| `onPageChange` | `(page: number) => void` | —       | Called whenever the page changes                                                            |
| `siblingCount` | `number`                 | `1`     | Number of page buttons to show on each side of the current page                             |

### Return Value

| Property      | Type                     | Description                                                       |
| ------------- | ------------------------ | ----------------------------------------------------------------- |
| `currentPage` | `number`                 | The currently active page (1-indexed)                             |
| `totalPages`  | `number`                 | Total number of pages                                             |
| `startIndex`  | `number`                 | Zero-based index of the first item on the current page            |
| `endIndex`    | `number`                 | Zero-based index of the last item on the current page (exclusive) |
| `canGoPrev`   | `boolean`                | Whether navigating to the previous page is possible               |
| `canGoNext`   | `boolean`                | Whether navigating to the next page is possible                   |
| `isFirstPage` | `boolean`                | Whether the current page is the first page                        |
| `isLastPage`  | `boolean`                | Whether the current page is the last page                         |
| `items`       | `PaginationPageItem[]`   | Ordered list of page items to render (see below)                  |
| `goTo`        | `(page: number) => void` | Navigate to a specific page number                                |
| `next`        | `() => void`             | Navigate to the next page                                         |
| `prev`        | `() => void`             | Navigate to the previous page                                     |
| `first`       | `() => void`             | Navigate to the first page                                        |
| `last`        | `() => void`             | Navigate to the last page                                         |

### `PaginationPageItem` Type

```typescript
type PaginationPageItem = { type: 'page'; page: number } | { type: 'ellipsis'; key: string };
```

The `items` array is ready to render directly — map over it and render `<PaginationLink>` for `type: 'page'` items and `<PaginationEllipsis>` for `type: 'ellipsis'` items.

### Uncontrolled Example

```tsx
import {
  usePagination,
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
} from 'xertica-ui/ui';

function MyTable({ data }: { data: Record<string, unknown>[] }) {
  const { currentPage, startIndex, endIndex, canGoPrev, canGoNext, items, next, prev, goTo } =
    usePagination({ totalItems: data.length, pageSize: 10 });

  const pageData = data.slice(startIndex, endIndex);

  return (
    <div className="space-y-4">
      <table>{/* render pageData rows */}</table>

      <Pagination>
        <PaginationContent>
          <PaginationItem>
            {/* Use disabled prop — not aria-disabled — for full a11y (removes href, tabIndex, pointer-events) */}
            <PaginationPrevious
              href="#"
              onClick={e => {
                e.preventDefault();
                prev();
              }}
              disabled={!canGoPrev}
            />
          </PaginationItem>

          {items.map(item =>
            item.type === 'ellipsis' ? (
              <PaginationItem key={item.key}>
                <PaginationEllipsis />
              </PaginationItem>
            ) : (
              <PaginationItem key={item.page}>
                <PaginationLink
                  href="#"
                  isActive={item.page === currentPage}
                  onClick={e => {
                    e.preventDefault();
                    goTo(item.page);
                  }}
                >
                  {item.page}
                </PaginationLink>
              </PaginationItem>
            )
          )}

          <PaginationItem>
            <PaginationNext
              href="#"
              onClick={e => {
                e.preventDefault();
                next();
              }}
              disabled={!canGoNext}
            />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </div>
  );
}
```

> **Page item algorithm (v2.1.9+)**: The `items` array is computed with a `Set`-based deduplication algorithm. Each page number appears exactly once regardless of `siblingCount` and total pages. Ellipsis keys are unique (`ellipsis-{page}`).

### Controlled Example (API-driven pagination)

```tsx
import { usePagination } from 'xertica-ui/ui';
import { useState } from 'react';

function ServerPaginatedTable({ totalItems }: { totalItems: number }) {
  const [page, setPage] = useState(1);

  const { items, canGoPrev, canGoNext, next, prev, goTo, currentPage } = usePagination({
    totalItems,
    pageSize: 20,
    page, // controlled
    onPageChange: setPage,
  });

  // fetch data for `page` from your API...

  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious
            href="#"
            onClick={e => {
              e.preventDefault();
              prev();
            }}
            disabled={!canGoPrev}
          />
        </PaginationItem>
        {items.map(item =>
          item.type === 'ellipsis' ? (
            <PaginationItem key={item.key}>
              <PaginationEllipsis />
            </PaginationItem>
          ) : (
            <PaginationItem key={item.page}>
              <PaginationLink
                href="#"
                isActive={item.page === currentPage}
                onClick={e => {
                  e.preventDefault();
                  goTo(item.page);
                }}
              >
                {item.page}
              </PaginationLink>
            </PaginationItem>
          )
        )}
        <PaginationItem>
          <PaginationNext
            href="#"
            onClick={e => {
              e.preventDefault();
              next();
            }}
            disabled={!canGoNext}
          />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  );
}
```

---

## AI Rules

- Place Pagination below the `<Table>` inside the same `<Card>` wrapping both.
- Use `isActive` on the currently selected page link.
- **Use `disabled` prop (not `aria-disabled` alone)** on `PaginationPrevious` and `PaginationNext` — `disabled={!canGoPrev}` / `disabled={!canGoNext}`. The `disabled` prop removes `href`, sets `tabIndex={-1}`, `aria-disabled`, and `pointer-events-none opacity-50` simultaneously for full accessibility.
- For controlled pagination (API-driven), use `onClick` with `e.preventDefault()` on `PaginationLink` instead of letting `href="#"` cause a scroll-to-top.
- When using `usePagination`, use `startIndex` and `endIndex` to slice client-side data arrays: `data.slice(startIndex, endIndex)`.
- For server-side pagination, pass `page` and `onPageChange` to `usePagination` to use controlled mode — the hook will not manage internal state.
- Always render `PaginationEllipsis` for `item.type === 'ellipsis'` items from the `items` array — never skip them. Ellipsis keys are unique strings in the form `ellipsis-{page}`.
- `totalItems` is the count of **all** records, not just the current page.
- Pages are **1-indexed** — the first page is `1`, not `0`.

---

## Related Components

- [`Table`](./table.md) — Primary context where Pagination is used
