{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "title": "Pagination",
  "description": "Pagination from the Get IT Done design system.",
  "dependencies": [
    "class-variance-authority@^0.7.0",
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/button-group.json",
    "https://zero-design.itdone.de/r/labels.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/loading-spinner.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/select.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/pagination.tsx",
      "content": "'use client';\n\nimport { cva } from 'class-variance-authority';\nimport { ArrowLeft, ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';\nimport * as React from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport { ButtonGroup, ButtonGroupItem } from '@/components/ui/button-group';\nimport { LoadingSpinner } from '@/components/ui/loading-spinner';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\nimport type { LabelProp } from '@/components/ui/labels';\n\n// Figma Pagination (1115:68622): Type Page default | Page minimal | Card default | Card minimal | Card button group | Lazy | Compact (zero),\n// Shape Square | Circle, State Default | Disabled | Loading (Lazy).\ntype PaginationVariant = 'page' | 'page-minimal' | 'card' | 'card-minimal' | 'card-button-group' | 'compact' | 'lazy';\ntype PaginationShape = 'square' | 'circle';\ntype PaginationAlign = 'left' | 'center' | 'right';\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end';\n\nconst range = (from: number, to: number) => Array.from({ length: Math.max(0, to - from + 1) }, (_, i) => from + i);\n\n// Page list with ellipses: boundary pages at both ends, a sibling window around the current page.\nconst getPaginationItems = (page: number, count: number, siblingCount = 1, boundaryCount = 1): PaginationItem[] => {\n  const startPages = range(1, Math.min(boundaryCount, count));\n  const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count);\n  const siblingsStart = Math.max(Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1), boundaryCount + 2);\n  const siblingsEnd = Math.min(Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2), endPages.length > 0 ? endPages[0] - 2 : count - 1);\n  return [\n    ...startPages,\n    ...(siblingsStart > boundaryCount + 2 ? (['ellipsis-start'] as const) : boundaryCount + 1 < count - boundaryCount ? [boundaryCount + 1] : []),\n    ...range(siblingsStart, siblingsEnd),\n    ...(siblingsEnd < count - boundaryCount - 1 ? (['ellipsis-end'] as const) : count - boundaryCount > boundaryCount ? [count - boundaryCount] : []),\n    ...endPages,\n  ];\n};\n\nconst frameVariants = cva('flex w-full items-center border-t border-solid border-border-default', {\n  variants: {\n    variant: {\n      page: 'justify-between pt-5',\n      'page-minimal': 'justify-between pt-5',\n      card: 'justify-between px-6 pb-4 pt-3',\n      'card-minimal': 'justify-between px-6 pb-4 pt-3',\n      'card-button-group': 'px-6 pb-4 pt-3',\n      lazy: 'justify-center gap-3 px-6 pb-4 pt-3',\n      compact: 'h-auto flex-wrap gap-2 sm:h-pager sm:flex-nowrap bg-card px-4 py-2',\n    },\n  },\n  defaultVariants: { variant: 'page' },\n});\n\nconst numberVariants = cva(\n  [\n    'inline-flex h-control-sm w-control-sm shrink-0 items-center justify-center transition-colors',\n    'hover:bg-muted hover:text-text-default',\n    'disabled:pointer-events-none disabled:opacity-disabled',\n    ...focusRing,\n  ],\n  {\n    variants: {\n      shape: { square: 'rounded-control', circle: 'rounded-full' },\n      // Compact (zero): idle pages are default text on no fill, the current page is a white chip with a hairline border.\n      tone: {\n        default: 'typography-label text-text-subtle aria-[current=page]:bg-muted aria-[current=page]:text-text-default',\n        // Zero page list (variant page and card): 13px pages, 12/500 current page, no 16px label role.\n        zeroPage: 'typography-ui-sm text-text-subtle aria-[current=page]:typography-detail aria-[current=page]:bg-muted aria-[current=page]:text-text-default',\n        // Compact before the zero preset: kept as shipped for every other theme.\n        compactLegacy:\n          'typography-label aria-[current=page]:bg-muted aria-[current=page]:text-text-default border border-solid border-transparent text-text-default hover:bg-muted aria-[current=page]:border-border-default aria-[current=page]:bg-background',\n        compact:\n          'typography-ui-sm aria-[current=page]:typography-detail border border-solid border-transparent text-text-default hover:bg-muted aria-[current=page]:border-border-default aria-[current=page]:bg-background',\n      },\n    },\n    defaultVariants: { shape: 'square', tone: 'default' },\n  },\n);\n\nexport interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {\n  variant?: PaginationVariant;\n  shape?: PaginationShape;\n  // Controlled current page (1 based).\n  page?: number;\n  defaultPage?: number;\n  onPageChange?: (page: number) => void;\n  // Total number of pages. Derived from `total` and `pageSize` when omitted.\n  pageCount?: number;\n  // Total number of records, used by `compact` (\"1 to 20 of 214\") and `lazy` (\"Showing 20 of 134\").\n  total?: number;\n  // Records per page (`compact`) or records loaded so far (`lazy`).\n  pageSize?: number;\n  // Options for the per-page selector in `compact`. Without `onPageSizeChange` the text is static.\n  pageSizeOptions?: number[];\n  onPageSizeChange?: (size: number) => void;\n  // Alignment of the actions for the minimal and button group cards.\n  align?: PaginationAlign;\n  // Number of pages shown on each side of the current page.\n  siblingCount?: number;\n  // Number of pages always shown at each end.\n  boundaryCount?: number;\n  // Disables every control.\n  disabled?: boolean;\n  // A load is in flight. `lazy`: spinner on Load more. Other variants: the summary reads \"Loading…\", the pager is `aria-busy` and its controls are disabled.\n  loading?: boolean;\n  // Text shown instead of the range or page summary while `loading`. Wins over the provider text.\n  loadingText?: string;\n  // Announce the summary changes politely. Set false when another live region already announces the load (a table first load), so the pager stays silent.\n  announce?: boolean;\n  // `lazy`: called by the Load more button.\n  onLoadMore?: () => void;\n  // `lazy`: no further records, hides the button.\n  exhausted?: boolean;\n  // Built-in texts. Each wins over the provider text; the function forms receive the numbers.\n  previousLabel?: string;\n  nextLabel?: string;\n  previousText?: string;\n  nextText?: string;\n  rowsPerPageLabel?: string;\n  pagesLabel?: string;\n  pageLabel?: LabelProp<[number]>;\n  pageOfText?: LabelProp<[number, number]>;\n  perPageText?: LabelProp<[number]>;\n  rangeText?: LabelProp<[number, number, number | undefined]>;\n  showingText?: LabelProp<[number, number]>;\n  loadMoreText?: string;\n  loadingMoreLabel?: string;\n}\n\nconst alignClass: Record<PaginationAlign, string> = { left: 'justify-start', center: 'justify-center', right: 'justify-end' };\n\nconst Pagination = React.forwardRef<HTMLElement, PaginationProps>(\n  (\n    {\n      className,\n      variant = 'page',\n      shape = 'square',\n      page: pageProp,\n      defaultPage = 1,\n      onPageChange,\n      pageCount: pageCountProp,\n      total,\n      pageSize = 20,\n      pageSizeOptions,\n      onPageSizeChange,\n      align = 'right',\n      siblingCount = 1,\n      boundaryCount = 1,\n      disabled = false,\n      loading = false,\n      announce = true,\n      onLoadMore,\n      exhausted = false,\n      'aria-label': ariaLabelProp,\n      previousLabel,\n      nextLabel,\n      previousText,\n      nextText,\n      rowsPerPageLabel,\n      pagesLabel,\n      pageLabel,\n      pageOfText,\n      perPageText,\n      rangeText,\n      showingText,\n      loadMoreText,\n      loadingMoreLabel,\n      loadingText,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels({\n      previousPage: previousLabel,\n      nextPage: nextLabel,\n      previous: previousText,\n      next: nextText,\n      rowsPerPage: rowsPerPageLabel,\n      pages: pagesLabel,\n      page: pageLabel,\n      pageOf: pageOfText,\n      perPage: perPageText,\n      range: rangeText,\n      showing: showingText,\n      loadMore: loadMoreText,\n      loadingMore: loadingMoreLabel,\n      loading: loadingText,\n    });\n    const ariaLabel = ariaLabelProp ?? l.pagination;\n    // Non-lazy pagers block navigation while a page loads\n    const zero = usePreset() === 'zero';\n    const pagerBusy = loading && variant !== 'lazy';\n    const off = disabled || pagerBusy;\n    const live = announce ? 'polite' : 'off';\n    const loadingSummary = (\n      <span className=\"inline-flex items-center gap-2\">\n        <LoadingSpinner size=\"sm\" role=\"presentation\" aria-hidden />\n        {`${l.loading}…`}\n      </span>\n    );\n    const [inner, setInner] = React.useState(defaultPage);\n    const controlled = pageProp !== undefined;\n    const pageCount = Math.max(1, pageCountProp ?? (total !== undefined ? Math.ceil(total / pageSize) : 1));\n    const page = Math.min(Math.max(1, controlled ? pageProp : inner), pageCount);\n    const goTo = (next: number) => {\n      const clamped = Math.min(Math.max(1, next), pageCount);\n      if (clamped === page) return;\n      if (!controlled) setInner(clamped);\n      onPageChange?.(clamped);\n    };\n    const items = getPaginationItems(page, pageCount, siblingCount, boundaryCount);\n    const atStart = page <= 1;\n    const atEnd = page >= pageCount;\n    const first = total === 0 ? 0 : (page - 1) * pageSize + 1;\n    const last = total === undefined ? page * pageSize : Math.min(page * pageSize, total);\n\n    const numberCells = (tone: 'default' | 'zeroPage' | 'compact' | 'compactLegacy' = 'default') =>\n      items.map((item) =>\n        typeof item === 'number' ? (\n          <li key={item}>\n            <button\n              type=\"button\"\n              disabled={off}\n              aria-label={l.page(item)}\n              aria-current={item === page ? 'page' : undefined}\n              onClick={() => goTo(item)}\n              className={numberVariants({ shape, tone })}\n            >\n              {item}\n            </button>\n          </li>\n        ) : (\n          <li key={item} aria-hidden=\"true\">\n            <span className={cn(numberVariants({ shape, tone }), 'pointer-events-none hover:bg-transparent')}>...</span>\n          </li>\n        ),\n      );\n\n    // The page and card lists show the numbers from sm up. Below sm they collapse to \"Page x of y\" so the row never overflows a 390px screen.\n    const pagesTone = variant === 'compact' ? (zero ? 'compact' : 'compactLegacy') : zero ? 'zeroPage' : 'default';\n    const collapsible = variant === 'page' || variant === 'card';\n    const pages = (\n      <>\n        <ol className={cn('flex items-center', variant === 'compact' ? cn('flex-wrap', zero ? 'gap-2' : 'gap-1') : 'gap-micro', collapsible && 'max-sm:hidden')}>{numberCells(pagesTone)}</ol>\n        {collapsible ? (\n          <span aria-live={live} className={cn('min-w-0 text-center text-text-default sm:hidden', zero ? 'typography-ui-sm' : 'typography-label')}>\n            {pagerBusy ? loadingSummary : l.pageOf(page, pageCount)}\n          </span>\n        ) : null}\n      </>\n    );\n\n    const textButtons = (kind: 'ghost' | 'outline') => (\n      <>\n        <Button type=\"button\" variant={kind} size=\"sm\" disabled={off || atStart} onClick={() => goTo(page - 1)} icon={<ArrowLeft />}>\n          {l.previous}\n        </Button>\n        {pages}\n        <Button type=\"button\" variant={kind} size=\"sm\" disabled={off || atEnd} onClick={() => goTo(page + 1)}>\n          {l.next}\n          <ArrowRight aria-hidden=\"true\" className=\"h-icon-md w-icon-md shrink-0\" />\n        </Button>\n      </>\n    );\n\n    let body: React.ReactNode;\n    if (variant === 'page') {\n      body = textButtons('ghost');\n    } else if (variant === 'card') {\n      body = textButtons('outline');\n    } else if (variant === 'page-minimal' || variant === 'card-minimal') {\n      // Minimal: \"Page x of y\" with Previous and Next, the group sits on the chosen side.\n      body = (\n        <>\n          <div aria-live={live} className={cn('typography-label text-text-default', align === 'center' && 'mx-auto')}>\n            {pagerBusy ? loadingSummary : l.pageOf(page, pageCount)}\n          </div>\n          <div className={cn('flex items-start gap-3', align === 'left' && 'order-first', align === 'center' && 'hidden')}>\n            <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={off || atStart} onClick={() => goTo(page - 1)}>\n              {l.previous}\n            </Button>\n            <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={off || atEnd} onClick={() => goTo(page + 1)}>\n              {l.next}\n            </Button>\n          </div>\n        </>\n      );\n      if (align === 'center') {\n        body = (\n          <div className=\"flex w-full items-center justify-center gap-3\">\n            <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={off || atStart} onClick={() => goTo(page - 1)}>\n              {l.previous}\n            </Button>\n            <div aria-live={live} className=\"typography-label text-text-default\">\n              {pagerBusy ? loadingSummary : l.pageOf(page, pageCount)}\n            </div>\n            <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={off || atEnd} onClick={() => goTo(page + 1)}>\n              {l.next}\n            </Button>\n          </div>\n        );\n      }\n    } else if (variant === 'card-button-group') {\n      body = (\n        <div className={cn('flex w-full', alignClass[align])}>\n          <ButtonGroup aria-label={l.pages}>\n            <ButtonGroupItem disabled={off || atStart} icon={<ArrowLeft />} onClick={() => goTo(page - 1)}>\n              {l.previous}\n            </ButtonGroupItem>\n            {items.map((item) =>\n              typeof item === 'number' ? (\n                <ButtonGroupItem\n                  key={item}\n                  disabled={off}\n                  aria-label={l.page(item)}\n                  aria-current={item === page ? 'page' : undefined}\n                  data-current={item === page ? true : undefined}\n                  // Below sm only the current page stays, so the group never overflows a 390px screen.\n                  className={cn('w-control-md px-0', item !== page && 'max-sm:hidden')}\n                  onClick={() => goTo(item)}\n                >\n                  {item}\n                </ButtonGroupItem>\n              ) : (\n                <span\n                  key={item}\n                  aria-hidden=\"true\"\n                  className=\"inline-flex h-control-md w-control-md items-center justify-center border-r border-solid border-input typography-label text-text-subtle max-sm:hidden\"\n                >\n                  ...\n                </span>\n              ),\n            )}\n            <ButtonGroupItem disabled={off || atEnd} onClick={() => goTo(page + 1)}>\n              {l.next}\n              <ArrowRight aria-hidden=\"true\" className=\"h-icon-md w-icon-md shrink-0\" />\n            </ButtonGroupItem>\n          </ButtonGroup>\n        </div>\n      );\n    } else if (variant === 'lazy') {\n      body = (\n        <>\n          <p aria-live={live} className=\"typography-label text-text-default\">\n            {l.showing(pageSize, total ?? pageSize)}\n          </p>\n          {!exhausted && (\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              disabled={disabled || loading}\n              aria-busy={loading || undefined}\n              onClick={() => onLoadMore?.()}\n            >\n              {loading && <LoadingSpinner size=\"sm\" label={l.loadingMore} />}\n              {l.loadMore}\n            </Button>\n          )}\n        </>\n      );\n    } else {\n      // compact (zero): summary, spacer, per page, arrows, pages\n      const sizes = pageSizeOptions ?? [pageSize];\n      body = (\n        <>\n          <div aria-live={live} className=\"typography-caption-regular shrink-0 text-text-subtle\">\n            {pagerBusy ? loadingSummary : l.range(first, last, total)}\n          </div>\n          <div className=\"flex-1\" />\n          {onPageSizeChange ? (\n            <Select value={String(pageSize)} onValueChange={(v) => onPageSizeChange(Number(v))} disabled={off}>\n              <SelectTrigger\n                aria-label={l.rowsPerPage}\n                className={cn('h-control-sm w-auto gap-1 border-transparent bg-transparent px-2 py-0 text-text-subtle shadow-none', zero ? 'typography-ui-sm' : 'typography-caption-regular')}\n              >\n                <SelectValue>{l.perPage(pageSize)}</SelectValue>\n              </SelectTrigger>\n              <SelectContent>\n                {sizes.map((s) => (\n                  <SelectItem key={s} value={String(s)}>\n                    {l.perPage(s)}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          ) : (\n            <p className=\"typography-caption-regular shrink-0 text-text-subtle\">{l.perPage(pageSize)}</p>\n          )}\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label={l.previousPage}\n            disabled={off || atStart}\n            onClick={() => goTo(page - 1)}\n            className={cn('h-control-sm w-control-sm', zero && 'disabled:!opacity-100 disabled:text-text-subtlest')}\n          >\n            <ChevronLeft aria-hidden=\"true\" className=\"h-icon-md w-icon-md\" />\n          </Button>\n          {pages}\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label={l.nextPage}\n            disabled={off || atEnd}\n            onClick={() => goTo(page + 1)}\n            className={cn('h-control-sm w-control-sm', zero && 'disabled:!opacity-100 disabled:text-text-subtlest')}\n          >\n            <ChevronRight aria-hidden=\"true\" className=\"h-icon-md w-icon-md\" />\n          </Button>\n        </>\n      );\n    }\n\n    return (\n      <nav\n        ref={ref}\n        aria-label={ariaLabel}\n        aria-busy={pagerBusy || undefined}\n        data-variant={variant}\n        data-shape={shape}\n        className={cn(frameVariants({ variant }), className)}\n        {...props}\n      >\n        {body}\n      </nav>\n    );\n  },\n);\nPagination.displayName = 'Pagination';\n\nexport { getPaginationItems, Pagination, type PaginationAlign, type PaginationItem, type PaginationShape, type PaginationVariant };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}