import type { HTMLAttributes, ReactNode } from 'react'
import { useRef, useState } from 'react'
import { IoChevronBack, IoChevronForward } from 'react-icons/io5'
import { cx } from '../../utils/cx'
import { usePressFeedback } from '../../hooks/usePressFeedback'
import { useSlidingThumb } from '../../motion/useSlidingThumb'
import { useContainerWidth } from '../../hooks/useContainerWidth'
import { rowWidth } from '../../utils/rowWidth'
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'
import type { MaySize } from '../../types'
/** The key lifts a little under the press, the way a segmented thumb does. */
const PRESS_SCALE = 1.14
/** xs is absent: a pager key that small stops being a touch target. */
export type PaginationSize = Exclude
/** A rendered slot: a page number, or a gap standing in for the pages between. */
type Slot = number | 'gap-start' | 'gap-end'
export interface PaginationProps extends Omit, 'onChange'> {
/** The current page, 1-based. */
page: number
pageCount: number
onPageChange: (page: number) => void
/** How many pages to show either side of the current one. @default 1 */
siblingCount?: number
/** @default 'md' */
size?: PaginationSize
/**
* Collapse to "Page 3 of 12" between the two arrows. Twelve keys do not fit
* across a phone, and a pager that scrolls sideways is worse than one that
* counts.
*
* Left off, the pager decides for itself: it measures the keys it would
* draw and collapses when they do not fit the space it has been given. Pass
* `true` to force the count, or `false` to insist on the keys and accept
* that a narrow container will have to scroll to reach them.
*/
compact?: boolean
/** Accessible name for the pager. @default 'Pagination' */
'aria-label'?: string
}
const range = (start: number, end: number): number[] =>
Array.from({ length: Math.max(0, end - start + 1) }, (_, i) => start + i)
/**
* Which keys to draw.
*
* The row is always the same width: two pinned ends and a run of pages between
* them, with the spare slot at either end turning into a gap when there is
* more beyond it. A pager that reflows under the finger as the ellipsis
* appears and disappears is exactly what that constant width avoids.
*
* Because the gap swallows the page it replaced, it never stands in for a
* single number — an ellipsis hiding one page is a lie that costs a tap.
*/
function slotsFor(page: number, pageCount: number, siblingCount: number): Slot[] {
// first + last + both gaps + the current page and its siblings
const windowSize = siblingCount * 2 + 5
if (pageCount <= windowSize) return range(1, pageCount)
const span = windowSize - 3
let start = page - siblingCount - 1
let end = page + siblingCount + 1
// Pinned to whichever end the window has run past, keeping its length.
if (start < 2) {
start = 2
end = start + span
} else if (end > pageCount - 1) {
end = pageCount - 1
start = end - span
}
const middle: Slot[] = range(start, end)
if (start > 2) middle[0] = 'gap-start'
if (end < pageCount - 1) middle[middle.length - 1] = 'gap-end'
return [1, ...middle, pageCount]
}
interface KeyProps {
children: ReactNode
label: string
current?: boolean
disabled?: boolean
onClick: () => void
className?: string
keyRef?: (node: HTMLButtonElement | null) => void
}
/** One key of the pager. Always a real `