{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"page-indicator","title":"Page Indicator","description":"Show which page of a carousel or a short sequence is current, as dots or line segments, and go to another page. Search terms: carousel dots, carousel indicator, pagination dots, slide indicator.","dependencies":[],"registryDependencies":["@siteplane/base","@siteplane/button"],"files":[{"path":"packages/ui/src/components/page-indicator/page-indicator.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\nimport type { PageIndicatorLabels } from \"../../system/locale/messages\";\nimport { useSiteplaneMessages } from \"../../system/locale/messages-context\";\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { Button } from \"../button\";\n\nimport \"./page-indicator.css\";\n\n/** A row of dots, or a line of segments that share the container's width. */\nexport type PageIndicatorVariant = \"dot\" | \"line\";\n\n/**\n * md: 8 px dots 12 px apart, or 6 px segments 8 px apart. lg: 10 px dots\n * 16 px apart, or 8 px segments 12 px apart.\n */\nexport type PageIndicatorSize = \"md\" | \"lg\";\n\nexport type { PageIndicatorLabels };\n\nexport interface PageIndicatorProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  /** The number of pages, one mark each. */\n  count: number;\n  /** The current page, counted from 1. */\n  page: number;\n  /** Called with the page a person picks, counted from 1. */\n  onPageChange?: ((page: number) => void) | undefined;\n  /** `dot`, the default, or `line`. */\n  variant?: PageIndicatorVariant | undefined;\n  /** `md`, the default, or `lg`. */\n  size?: PageIndicatorSize | undefined;\n  /** Sets the marks on a translucent pill, for an indicator on an image. */\n  framed?: boolean | undefined;\n  /**\n   * Replaces the names of the group and of its pages for this indicator, each\n   * one on its own, over the messages of SiteplaneLocaleProvider (English by\n   * default).\n   */\n  labels?: Partial<PageIndicatorLabels> | undefined;\n}\n\n/**\n * Shows which of a few pages is current, as a row of dots or a line of\n * segments, and goes to another page on a click. Each page is the public\n * Button as a square inline action, the bare mark in a larger target: the\n * Button owns its cursor and its states, and keyboard focus draws the kit\n * outline around the mark (page-indicator.css). The application keeps the\n * page in state and changes the content when `onPageChange` fires.\n */\nexport function PageIndicator({\n  className,\n  count,\n  framed = false,\n  labels: labelsProp,\n  onPageChange,\n  page,\n  size = \"md\",\n  variant = \"dot\",\n  ...props\n}: PageIndicatorProps): React.ReactElement {\n  const messages = useSiteplaneMessages(\"pageIndicator\");\n  const labels: PageIndicatorLabels = {\n    group: labelsProp?.group ?? messages.group,\n    page: labelsProp?.page ?? messages.page,\n  };\n  const pages = Array.from(\n    { length: Math.max(0, Math.floor(count)) },\n    (_, index) => index + 1,\n  );\n  return (\n    <div\n      aria-label={labels.group}\n      className={cn(slotClass(\"page-indicator\", props), className)}\n      data-framed={framed ? \"\" : undefined}\n      data-size={size}\n      data-slot=\"page-indicator\"\n      data-variant={variant}\n      role=\"group\"\n      {...props}\n    >\n      {pages.map((value) => {\n        const current = value === page;\n        return (\n          <Button\n            aria-current={current ? \"page\" : undefined}\n            aria-label={labels.page(value)}\n            // The Button widens its target to 44 px on a touch screen. The\n            // marks stand closer than that, so the target keeps the width of\n            // its mark's share of the row and only grows in height, as the\n            // Buttons of a Group do.\n            className=\"pointer-coarse:after:min-w-auto\"\n            data-current={current ? \"\" : undefined}\n            data-slot=\"page-indicator-item\"\n            key={value}\n            onClick={() => {\n              if (!current) onPageChange?.(value);\n            }}\n            profile=\"inline\"\n            shape=\"square\"\n            size=\"icon-2xs\"\n            variant=\"link\"\n          >\n            <span\n              className=\"sp-page-indicator-mark\"\n              data-slot=\"page-indicator-mark\"\n            />\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/page-indicator/page-indicator.tsx"},{"path":"packages/ui/src/components/page-indicator/page-indicator.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* A PageIndicator shows which of a few pages is current, as a row of dots or\n   * a line of segments. md: 8 px dots 12 px\n   * apart, or 6 px segments 8 px apart; lg: 10 px dots 16 px apart, or 8 px\n   * segments 12 px apart. A row of dots is as wide as its dots; a line takes\n   * the width of its container and its segments share it. The defaults sit at\n   * zero specificity, so the size and variant rules refine them (P6). */\n  :where(.sp-page-indicator) {\n    --sp-page-indicator-mark: var(--sp-rw-space-2);\n    --sp-page-indicator-gap: var(--sp-rw-space-3);\n    --sp-page-indicator-reach: var(--sp-rw-space-2);\n\n    display: flex;\n    width: max-content;\n    align-items: center;\n    gap: var(--sp-page-indicator-gap);\n  }\n\n  :root .sp-page-indicator[data-size=\"lg\"] {\n    --sp-page-indicator-mark: var(--sp-rw-space-2-5);\n    --sp-page-indicator-gap: var(--sp-rw-space-4);\n    --sp-page-indicator-reach: var(--sp-rw-space-3);\n  }\n\n  :root .sp-page-indicator[data-variant=\"line\"] {\n    --sp-page-indicator-mark: var(--sp-rw-space-1-5);\n    --sp-page-indicator-gap: var(--sp-rw-space-2);\n\n    width: 100%;\n  }\n\n  :root .sp-page-indicator[data-variant=\"line\"][data-size=\"lg\"] {\n    --sp-page-indicator-mark: var(--sp-rw-space-2);\n    --sp-page-indicator-gap: var(--sp-rw-space-3);\n  }\n\n  /* Framed, the marks sit on a pill of the dialog surface at 90 % with an\n   * 8 px blur, for an indicator on an image. Its padding is the reach of the\n   * targets, 8 px at md and 12 px at lg, so they end at its edge. */\n  :root .sp-page-indicator[data-framed] {\n    border-radius: var(--sp-rw-radius-full);\n    padding: var(--sp-page-indicator-reach);\n    background: color-mix(in oklab, var(--sp-rw-bg-secondary) 90%, transparent);\n    -webkit-backdrop-filter: blur(8px);\n    backdrop-filter: blur(8px);\n  }\n\n  /* Each page is the public Button as a square inline action, the bare mark in\n   * a larger target (T3). Its box is that target: the mark with half the gap\n   * on each side, and the reach above and below it, 8 px at md and 12 px at\n   * lg. An equal negative margin keeps the marks where the gap puts them, as\n   * the Stepper's trigger keeps its marker, so the targets reach past the row\n   * without moving it. Keyboard focus draws the kit outline around the mark\n   * through the rules that the square inline action shares with the field\n   * action, and the Button owns the cursor and the states. */\n  :root .sp-page-indicator-item[data-chassis][data-size] {\n    width: calc(var(--sp-page-indicator-mark) + var(--sp-page-indicator-gap));\n    height: calc(\n      var(--sp-page-indicator-mark) + var(--sp-page-indicator-reach) * 2\n    );\n    margin-block: calc(var(--sp-page-indicator-reach) * -1);\n    margin-inline: calc(var(--sp-page-indicator-gap) / -2);\n    border-radius: var(--sp-rw-radius-full);\n    padding-block: var(--sp-page-indicator-reach);\n    padding-inline: calc(var(--sp-page-indicator-gap) / 2);\n  }\n\n  /* A segment takes an equal share of the line. */\n  :root\n    .sp-page-indicator[data-variant=\"line\"]\n    > .sp-page-indicator-item[data-chassis][data-size] {\n    flex: 1 1 0;\n    width: auto;\n    min-width: 0;\n  }\n\n  :root\n    .sp-page-indicator[data-variant=\"line\"]\n    > .sp-page-indicator-item\n    > .sp-button-content {\n    flex: 1 1 0;\n  }\n\n  /* A mark rests in the track colour of Progress, and the current page takes\n   * its fill, which fades over the slow step as the carousel dots of the\n   * modal block do. */\n  :root .sp-page-indicator-mark {\n    width: var(--sp-page-indicator-mark);\n    height: var(--sp-page-indicator-mark);\n    flex: none;\n    border-radius: var(--sp-rw-radius-full);\n    background: var(--sp-rw-progress-track);\n    transition: background-color var(--motion-duration-slow)\n      var(--motion-ease-state);\n  }\n\n  :root .sp-page-indicator[data-variant=\"line\"] .sp-page-indicator-mark {\n    width: auto;\n    flex: 1 1 0;\n  }\n\n  :root .sp-page-indicator-item[data-current] .sp-page-indicator-mark {\n    background: var(--sp-rw-progress-fill);\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/page-indicator/page-indicator.css"},{"path":"packages/ui/src/components/page-indicator/index.ts","content":"export * from \"./page-indicator\";\n","type":"registry:ui","target":"~/src/siteplane/components/page-indicator/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/page-indicator.md","categories":["Navigation"],"meta":{"avoidWhen":["Avoid for a long list or table with numbered pages; use Pagination.","Avoid for the steps of a task that people complete in order; use Stepper."],"description":"Show which page of a carousel or a short sequence is current, as dots or line segments, and go to another page.","knownLimitations":["The indicator moves no content: keep the page in state and change the slide when onPageChange fires.","It shows one mark per page and no ellipsis; use Pagination beyond about ten pages.","The md dots sit 20 px apart, closer than the 24 px target spacing of WCAG 2.2; use lg where people tap them."],"providerNotes":[],"releaseStatus":"preview","searchTerms":["page indicator","carousel dots","carousel indicator","pagination dots","slide indicator","dots"],"useWhen":["Use below a carousel, onboarding slides or a short run of cards to show the current page and jump to another one.","Use framed when the indicator sits on an image, so the marks stay readable."]}}