{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:ui",
  "title": "Pagination",
  "description": "여러 페이지로 나뉜 목록에서 볼 페이지를 고르는 컨트롤입니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-pagination-window",
    "@atom/lib-use-controllable-state"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/pagination.tsx",
      "content": "\"use client\";\n\nimport { type HTMLAttributes, type MouseEvent, type Ref } from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\nimport { clampPageIndex, getPageWindow, normalizePageCount } from \"../lib/pagination-window\";\n\n/* -------------------------------------------------------------------------- */\n/*  Pagination (components/pagination.md, styles/atom/elements/pagination.css) */\n/*    <Pagination pageCount={24} pageIndex={page} onPageChange={setPage} />    */\n/*    <Pagination pageCount={5} size=\"small\" label=\"첨부파일 페이지\" />          */\n/*  pageIndex 는 0부터, 화면 번호는 1부터. Grid Footer 에서도 그대로 씁니다.     */\n/* -------------------------------------------------------------------------- */\n\nexport type PaginationSize = \"small\" | \"medium\";\n\nconst B = \"atom-pagination\";\n\nexport type PaginationProps = Omit<HTMLAttributes<HTMLElement>, \"onChange\"> & {\n  ref?: Ref<HTMLElement>;\n  /** 현재 페이지 인덱스입니다. 0부터 시작합니다. 주면 controlled 입니다. */\n  pageIndex?: number;\n  /** uncontrolled 일 때의 처음 페이지 인덱스입니다. 0부터 시작합니다. */\n  defaultPageIndex?: number;\n  /** 전체 페이지 수입니다. 0이면 1로 봅니다. */\n  pageCount: number;\n  /** 한 번에 노출할 번호 개수입니다. */\n  maxPageButtons?: number;\n  /** 컨트롤 높이와 폰트 크기를 결정합니다. */\n  size?: PaginationSize;\n  /** `<nav>` 의 `aria-label` 입니다. */\n  label?: string;\n  /** 페이지가 바뀔 때 호출됩니다. */\n  onPageChange?: (pageIndex: number) => void;\n};\n\n/* 원본 데모의 인라인 SVG (스프라이트에 없음) */\nconst ICON_PATHS = {\n  first: \"M12 3L7 8l5 5M4 3v10\",\n  prev: \"M10 3L5 8l5 5\",\n  next: \"M6 3l5 5-5 5\",\n  last: \"M4 3l5 5-5 5M12 3v10\",\n} as const;\n\nfunction EdgeIcon({ d }: { d: string }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n      <path d={d} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction PaginationRoot({\n  ref,\n  pageIndex,\n  defaultPageIndex = 0,\n  pageCount,\n  maxPageButtons = 10,\n  size = \"medium\",\n  label = \"페이지\",\n  onPageChange,\n  className,\n  ...rest\n}: PaginationProps) {\n  const count = normalizePageCount(pageCount);\n  const [rawIndex, setIndex] = useControllableState({\n    value: pageIndex,\n    defaultValue: defaultPageIndex,\n    onChange: onPageChange,\n  });\n  const index = clampPageIndex(rawIndex, count);\n  const last = count - 1;\n  const atStart = index === 0;\n  const atEnd = index === last;\n  const pages = getPageWindow(index, count, maxPageButtons);\n  const single = count === 1;\n\n  const go = (next: number) => (e: MouseEvent<HTMLButtonElement>) => {\n    // aria-disabled 는 포커스·Enter 를 막지 않으므로 여기서 무시합니다.\n    if (e.currentTarget.getAttribute(\"aria-disabled\") === \"true\") return;\n    const target = clampPageIndex(next, count);\n    if (target !== index) setIndex(target);\n  };\n\n  const edge = (kind: keyof typeof ICON_PATHS, ariaLabel: string, target: number, disabled: boolean) => (\n    <button\n      type=\"button\"\n      className={`${B}__btn`}\n      aria-label={ariaLabel}\n      aria-disabled={disabled ? \"true\" : undefined}\n      onClick={go(target)}\n    >\n      <EdgeIcon d={ICON_PATHS[kind]} />\n    </button>\n  );\n\n  return (\n    <nav {...rest} ref={ref} aria-label={label} className={cn(B, mod(B, \"size\", size, \"medium\"), className)}>\n      {edge(\"first\", \"처음으로\", 0, atStart)}\n      {edge(\"prev\", \"이전으로\", index - 1, atStart)}\n      {pages.map((p) => (\n        <button\n          key={p}\n          type=\"button\"\n          className={`${B}__page`}\n          aria-current={p === index ? \"page\" : undefined}\n          aria-disabled={single ? \"true\" : undefined}\n          onClick={go(p)}\n        >\n          {p + 1}\n        </button>\n      ))}\n      {edge(\"next\", \"다음으로\", index + 1, atEnd)}\n      {edge(\"last\", \"마지막으로\", last, atEnd)}\n    </nav>\n  );\n}\n\nexport const Pagination = Object.assign(PaginationRoot, {});\n",
      "type": "registry:ui",
      "target": "components/ui/pagination.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/grid.css",
      "content": "/* ============================================================================\n   Element — Grid (Data Table)\n   ----------------------------------------------------------------------------\n   A paged data table. The header stays put while the body scrolls, columns can\n   be sorted, moved, resized, pinned and filtered, and the footer reports the\n   row count, the pager and the page size in one line.\n\n   atom-grid\n     --size_{small|medium}                   medium is the default\n     [data-state=\"ready|loading|empty\"]\n     __scroll  __table  __thead  __tbody\n     __th  __th-inner  __th-label  __th-sort  __th-filter  __th-grip\n     __th-resize\n     __tr  __td  __cell-check\n     __tree-cell  __tree-caret  __tree-count    the hierarchy column\n     __check                                 header + row checkbox\n     __filter-popover  __filter-group  __filter-title  __filter-row\n     __filter-list  __filter-divider  __filter-actions\n     __overlay  __overlay-text  __spinner  __empty  __empty-text\n     __footer  __count  __count-value  __page-size\n\n   The footer carries all three status controls in one row —\n   총 건수 · Pagination · Page size, left to right. The middle one is the\n   Pagination element (styles/elements/pagination.css), not a part of this file.\n\n   Column modifiers live on both __th and __td so a pinned column stays lined\n   up through the body:\n     --pinned          sticky to the inline start edge\n     --sorted_asc  --sorted_desc\n     --align_{end|center}\n     --check           the row-selection column\n\n   __td--tree is the exception: it sits on the body cell only. The header of a\n   tree column needs no indent, and a modifier with no rule behind it is a\n   promise the stylesheet does not keep.\n\n   Tree rows are flat <tr>s, not nested ones — a table cannot nest and stay a\n   table. Depth arrives as an inline custom property on the row, the way\n   tree.css does it, because a `--depth_1 … --depth_9` ladder would cap the\n   hierarchy at whatever number this file stopped at. Collapsed descendants are\n   not hidden with CSS; the row model stops emitting them.\n\n   The checkbox repeats the Checkbox visual rather than importing it, for the\n   same reason Multi Select does: a card preview loads one stylesheet.\n   ========================================================================== */\n\n.atom-grid {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  min-width: 0;\n  font-size: 14px;\n  color: var(--atom-fg-neutral);\n}\n\n/* --- scroll container ---------------------------------------------------- */\n/* Vertical scroll lives here rather than on the page so the sticky header has\n   something to stick to; horizontal scroll appears on its own once the columns\n   outgrow the box.\n\n   `flex: 1 1 auto` with an auto basis means the box sizes to its content when\n   the Grid has no height of its own, and absorbs the leftover space when it\n   does — so `height: 300px` on .atom-grid is enough to pin the whole thing. */\n.atom-grid__scroll {\n  position: relative;\n  flex: 1 1 auto;\n  min-height: 0;\n  overflow: auto;\n  max-height: 420px;\n  border-radius: var(--atom-radius-r2);\n  box-shadow: inset 0 0 0 1px var(--atom-border-subtle);\n}\n\n.atom-grid__table {\n  width: 100%;\n  border-collapse: separate;\n  border-spacing: 0;\n  table-layout: fixed;\n}\n\n/* --- header -------------------------------------------------------------- */\n.atom-grid__thead .atom-grid__th {\n  position: sticky;\n  top: 0;\n  z-index: 2;\n  height: 44px;\n  padding: 0;\n  background: var(--atom-bg-neutral-weak);\n  border-bottom: 1px solid var(--atom-border-default);\n  font-size: 13px;\n  font-weight: 600;\n  color: var(--atom-fg-neutral);\n  text-align: left;\n  white-space: nowrap;\n}\n\n.atom-grid__th-inner {\n  position: relative;\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  height: 100%;\n  padding-inline: var(--atom-x3);\n}\n\n.atom-grid__th-label {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n/* --- sort (C) — 내림차 → 오름차 → 기본값 --------------------------------- */\n.atom-grid__th-sort {\n  display: inline-flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 1px;\n  width: 14px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-subtle);\n  cursor: pointer;\n}\n\n.atom-grid__th-sort svg {\n  width: 8px;\n  height: 5px;\n  display: block;\n}\n\n.atom-grid__th--sorted_desc .atom-grid__th-sort,\n.atom-grid__th--sorted_asc .atom-grid__th-sort {\n  color: var(--atom-fg-neutral);\n}\n\n/* Only the active direction stays solid — a sorted column that highlights both\n   arrows tells the user nothing about which way it went. */\n.atom-grid__th--sorted_desc .atom-grid__th-sort svg:first-child,\n.atom-grid__th--sorted_asc .atom-grid__th-sort svg:last-child {\n  opacity: 0.25;\n}\n\n/* --- filter (F) ---------------------------------------------------------- */\n.atom-grid__th-filter {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 20px;\n  height: 20px;\n  flex-shrink: 0;\n  border-radius: var(--atom-radius-r1);\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-grid__th-filter svg {\n  width: 13px;\n  height: 13px;\n}\n\n.atom-grid__th-filter:hover {\n  background: var(--atom-bg-layer);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-grid__th-filter[data-active] {\n  color: var(--atom-fg-brand);\n}\n\n.atom-grid__th-filter[aria-expanded=\"true\"] {\n  background: var(--atom-bg-layer);\n  color: var(--atom-fg-neutral);\n}\n\n/* --- column filter popover (F) ------------------------------------------- */\n/* Anchored to .atom-grid, not to the header cell: __scroll clips its own\n   overflow, so a popover parented inside a cell would be cut off the moment it\n   grew taller than the header. Production code positions it against the cell's\n   rect and closes it on focusout or Esc. */\n.atom-grid__filter-popover {\n  position: absolute;\n  z-index: 5;\n  display: flex;\n  flex-direction: column;\n  gap: var(--atom-x3);\n  width: 280px;\n  padding: var(--atom-x4);\n  border-radius: var(--atom-radius-r3);\n  background: var(--atom-bg-layer-raised);\n  box-shadow: var(--atom-shadow-s3), inset 0 0 0 1px var(--atom-border-subtle);\n  font-size: 13px;\n  text-align: left;\n  font-weight: 400;\n}\n\n.atom-grid__filter-popover[data-state=\"closed\"] {\n  display: none;\n}\n\n.atom-grid__filter-group {\n  display: flex;\n  flex-direction: column;\n  gap: var(--atom-x2);\n}\n\n.atom-grid__filter-title {\n  font-size: 11px;\n  font-weight: 600;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-grid__filter-row {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n}\n\n/* Both children default to width:100%, so without explicit flex the two of\n   them ask for more than the row has and shrink together — squeezing the mode\n   picker until its label breaks mid-word. The mode has a fixed job and a fixed\n   width; the search box takes what is left. */\n.atom-grid__filter-row .atom-select {\n  flex: 0 0 96px;\n}\n\n.atom-grid__filter-row .atom-text-input {\n  flex: 1 1 auto;\n  min-width: 0;\n}\n\n/* The value list scrolls on its own so the popover keeps a predictable height\n   no matter how many distinct values the column holds. */\n.atom-grid__filter-list {\n  display: flex;\n  flex-direction: column;\n  max-height: 84px;\n  overflow-y: auto;\n}\n\n.atom-grid__filter-divider {\n  height: 1px;\n  background: var(--atom-border-subtle);\n}\n\n.atom-grid__filter-actions {\n  display: flex;\n  justify-content: flex-end;\n  gap: var(--atom-x2);\n}\n\n/* --- move (B) ------------------------------------------------------------ */\n.atom-grid__th-grip {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 14px;\n  height: 20px;\n  flex-shrink: 0;\n  margin-inline-start: -4px;\n  color: var(--atom-fg-neutral-subtle);\n  cursor: grab;\n  opacity: 0;\n  transition: opacity var(--atom-duration-fast) ease;\n}\n\n.atom-grid__th-grip svg {\n  width: 10px;\n  height: 14px;\n}\n\n.atom-grid__th:hover .atom-grid__th-grip {\n  opacity: 1;\n}\n\n.atom-grid__th[data-dragging] {\n  background: var(--atom-bg-layer-raised);\n}\n\n.atom-grid__th[data-drop-target] {\n  box-shadow: inset 2px 0 0 var(--atom-fg-brand);\n}\n\n/* --- resize (D) ---------------------------------------------------------- */\n/* The handle is wider than the line it draws — a 1px hit area is unusable with\n   a mouse and impossible with a trackpad. */\n.atom-grid__th-resize {\n  position: absolute;\n  inset-block: 0;\n  inset-inline-end: -4px;\n  width: 9px;\n  cursor: col-resize;\n  z-index: 1;\n}\n\n.atom-grid__th-resize::after {\n  content: \"\";\n  position: absolute;\n  inset-block: 10px;\n  inset-inline-start: 4px;\n  width: 1px;\n  background: var(--atom-border-default);\n}\n\n.atom-grid__th-resize:hover::after,\n.atom-grid__th-resize[data-dragging]::after {\n  inset-block: 0;\n  width: 2px;\n  background: var(--atom-fg-brand);\n}\n\n/* --- body ---------------------------------------------------------------- */\n.atom-grid__td {\n  height: 44px;\n  padding-inline: var(--atom-x3);\n  border-bottom: 1px solid var(--atom-border-subtle);\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  background: var(--atom-bg-layer);\n}\n\n.atom-grid__tr:last-child .atom-grid__td {\n  border-bottom: 0;\n}\n\n.atom-grid__tr:hover .atom-grid__td {\n  background: var(--atom-bg-nav-hover);\n}\n\n.atom-grid__tr[aria-selected=\"true\"] .atom-grid__td {\n  background: var(--atom-bg-informative-weak);\n}\n\n.atom-grid__th--align_end,\n.atom-grid__td--align_end {\n  text-align: right;\n}\n\n.atom-grid__th--align_end .atom-grid__th-inner {\n  flex-direction: row-reverse;\n}\n\n.atom-grid__th--align_center,\n.atom-grid__td--align_center {\n  text-align: center;\n}\n\n/* Numbers are compared down the column, so they line up digit by digit. */\n.atom-grid__td--align_end {\n  font-variant-numeric: tabular-nums;\n}\n\n/* --- pinned (E) ---------------------------------------------------------- */\n.atom-grid__th--pinned,\n.atom-grid__td--pinned {\n  position: sticky;\n  inset-inline-start: 0;\n  z-index: 1;\n}\n\n.atom-grid__thead .atom-grid__th--pinned {\n  z-index: 3;\n}\n\n/* The row washes have to stay opaque on a pinned cell.\n   `--atom-bg-nav-hover` is 4% brand — a tint, not a fill. Behind a normal cell\n   that lands on a static surface and reads as intended, but a pinned cell is\n   the only thing standing between the reader and the columns sliding\n   underneath it: replacing its opaque `--atom-bg-layer` with a 4% wash lets\n   the scrolled text through and the two sets of text collide. So the surface\n   stays the background *colour* and the wash is painted as an image over it.\n\n   Selection outranks hover here the same way it does on a plain cell, and it\n   needs no layering — `--atom-bg-informative-weak` is already opaque. */\n.atom-grid__tr:hover .atom-grid__td--pinned {\n  background-color: var(--atom-bg-layer);\n  background-image: linear-gradient(\n    var(--atom-bg-nav-hover),\n    var(--atom-bg-nav-hover)\n  );\n}\n\n.atom-grid__tr[aria-selected=\"true\"] .atom-grid__td--pinned,\n.atom-grid__tr[aria-selected=\"true\"]:hover .atom-grid__td--pinned {\n  background-color: var(--atom-bg-informative-weak);\n  background-image: none;\n}\n\n/* The shadow only appears once the grid is actually scrolled sideways —\n   a permanent seam reads as a border the user cannot explain. */\n.atom-grid[data-scrolled-x] .atom-grid__th--pinned::after,\n.atom-grid[data-scrolled-x] .atom-grid__td--pinned::after {\n  content: \"\";\n  position: absolute;\n  inset-block: 0;\n  inset-inline-end: 0;\n  width: 6px;\n  transform: translateX(100%);\n  background: linear-gradient(\n    to right,\n    rgb(0 0 0 / 0.08),\n    rgb(0 0 0 / 0)\n  );\n  pointer-events: none;\n}\n\n/* --- row checkbox (A) ---------------------------------------------------- */\n.atom-grid__th--check,\n.atom-grid__td--check {\n  width: 44px;\n  padding-inline: 0;\n  text-align: center;\n}\n\n.atom-grid__th--check .atom-grid__th-inner {\n  justify-content: center;\n  padding-inline: 0;\n}\n\n.atom-grid__check {\n  position: relative;\n  display: inline-block;\n  width: 18px;\n  height: 18px;\n  vertical-align: middle;\n  border-radius: var(--atom-radius-r1);\n  background: var(--atom-bg-layer);\n  box-shadow: inset 0 0 0 1.5px var(--atom-border-default);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    box-shadow var(--atom-duration-fast) ease;\n}\n\n.atom-grid__check svg,\n.atom-grid__check i {\n  position: absolute;\n  inset: 0;\n  margin: auto;\n  color: var(--atom-fg-on-solid);\n  opacity: 0;\n}\n\n.atom-grid__check svg {\n  width: 12px;\n  height: 12px;\n}\n\n.atom-grid__check i {\n  width: 9px;\n  height: 2px;\n  border-radius: 1px;\n  background: var(--atom-fg-on-solid);\n}\n\n.atom-grid__check[data-state=\"checked\"],\n.atom-grid__check[data-state=\"indeterminate\"] {\n  background: var(--atom-bg-neutral-solid);\n  box-shadow: inset 0 0 0 1.5px var(--atom-bg-neutral-solid);\n}\n\n.atom-grid__check[data-state=\"checked\"] svg,\n.atom-grid__check[data-state=\"indeterminate\"] i {\n  opacity: 1;\n}\n\n/* --- loading (5) --------------------------------------------------------- */\n.atom-grid__overlay {\n  position: absolute;\n  inset: 0;\n  z-index: 4;\n  display: none;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: var(--atom-x3);\n  background: var(--atom-bg-scrim);\n  border-radius: var(--atom-radius-r2);\n}\n\n.atom-grid[data-state=\"loading\"] .atom-grid__overlay {\n  display: flex;\n}\n\n.atom-grid__overlay-text {\n  font-size: 13px;\n  color: var(--atom-fg-neutral);\n}\n\n.atom-grid__spinner {\n  width: 28px;\n  height: 28px;\n  border-radius: var(--atom-radius-full);\n  border: 3px solid var(--atom-border-default);\n  border-top-color: var(--atom-fg-brand);\n  animation: atom-grid-spin var(--atom-duration-slow) linear infinite;\n}\n\n@keyframes atom-grid-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .atom-grid__spinner {\n    animation-duration: 2.4s;\n  }\n}\n\n/* --- empty (6) ----------------------------------------------------------- */\n.atom-grid__empty {\n  display: none;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: var(--atom-x2);\n  min-height: 180px;\n  padding: var(--atom-x8) var(--atom-x4);\n  background: var(--atom-bg-layer);\n}\n\n.atom-grid[data-state=\"empty\"] .atom-grid__empty {\n  display: flex;\n}\n\n.atom-grid[data-state=\"empty\"] .atom-grid__tbody {\n  display: none;\n}\n\n.atom-grid__empty-text {\n  font-size: 14px;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- footer — 총 건수 (1) + pager (2) + page size (3) --------------------- */\n/* One row, three slots: count left, pager centred, page size right. Only the\n   two ends are styled here — the pager in the middle is its own element, and\n   what this file still owns about it is placement. See the rule below. */\n.atom-grid__footer {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x4);\n  padding-top: var(--atom-x3);\n}\n\n.atom-grid__count {\n  flex-shrink: 0;\n  font-size: 13px;\n  color: var(--atom-fg-neutral-muted);\n}\n\n/* The count is the one number a user checks against their own expectation, so\n   it stays legible while the label around it recedes. */\n.atom-grid__count-value {\n  font-weight: 600;\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral);\n}\n\n.atom-grid__page-size {\n  flex-shrink: 0;\n  width: 96px;\n}\n\n/* The pager is Pagination — styles/elements/pagination.css. The block moved\n   out of this file once a list that is not a table needed the same control,\n   and it deliberately carries no margin of its own, so where it sits is the\n   caller's business. Here it is centred against the Grid rather than against\n   the gap between its neighbours: the count growing a digit must not shift the\n   page numbers out from under a hand about to press the same button twice. */\n.atom-grid__footer .atom-pagination {\n  margin-inline: auto;\n}\n\n/* --- tree ----------------------------------------------------------------\n   One column carries the hierarchy; every other column stays a plain cell.\n\n   The indent is padding on the cell, not a margin on its contents, so the row\n   fill — hover, selection, the zebra of the border — still runs the full width\n   of the column. Content pushed in by a margin leaves the fill starting at the\n   indent, and the rows read as a set of disconnected boxes.\n\n   20px per level is tree.css's `--atom-tree-indent`. The two controls sit side\n   by side in the same screens, and a hierarchy that steps 20px in a Tree and\n   something else in a Grid reads as two different depths. */\n.atom-grid {\n  --atom-grid-tree-indent: 20px;\n}\n\n/* The fallback is not decoration: a virtualised body renders rows into a flat\n   container that may not have .atom-grid above it, and a row with no depth\n   still has to draw at level 0. */\n.atom-grid__td--tree {\n  padding-inline-start: calc(\n    var(--atom-x3) + var(--atom-grid-tree-depth, 0) *\n      var(--atom-grid-tree-indent, 20px)\n  );\n}\n\n/* The cell is a flex row so the caret keeps its box while the label truncates.\n   __td already sets `overflow: hidden`, so the ellipsis lands on the label. */\n.atom-grid__tree-cell {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  min-width: 0;\n}\n\n/* A leaf keeps the slot and drops the glyph — the same trade tree.css makes.\n   Removing the box would pull every leaf 16px left of its siblings, and the\n   indent is the only thing saying which parent a row belongs to. */\n.atom-grid__tree-caret {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 16px;\n  height: 16px;\n  flex-shrink: 0;\n  border: 0;\n  padding: 0;\n  background: none;\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n  transition: transform var(--atom-duration-fast) var(--atom-ease-out);\n}\n\n.atom-grid__tree-caret svg {\n  display: block;\n  width: 14px;\n  height: 14px;\n}\n\n.atom-grid__tree-caret--empty {\n  pointer-events: none;\n  visibility: hidden;\n}\n\n.atom-grid__tree-caret:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: 1px;\n  border-radius: var(--atom-radius-r1);\n}\n\n.atom-grid__tr[aria-expanded=\"true\"] .atom-grid__tree-caret {\n  transform: rotate(90deg);\n}\n\n/* The label owns the truncation, not the cell: an ellipsis on __td would clip\n   the count along with the name. */\n.atom-grid__tree-label {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n/* Muted and a step down, because it is a fact about the row rather than the\n   row's name. `tabular-nums` so the number stops shifting as branches open. */\n.atom-grid__tree-count {\n  flex-shrink: 0;\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral-muted);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-grid--size_small {\n  /* One step narrower with the rows, so the staircase keeps the same slope\n     against a 36px row that it has against a 44px one. */\n  --atom-grid-tree-indent: 16px;\n\n  font-size: 13px;\n}\n\n.atom-grid--size_small .atom-grid__thead .atom-grid__th {\n  height: 36px;\n  font-size: 12px;\n}\n\n.atom-grid--size_small .atom-grid__td {\n  height: 36px;\n}\n\n.atom-grid--size_small .atom-grid__th--check,\n.atom-grid--size_small .atom-grid__td--check {\n  width: 36px;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/grid.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/pagination.css",
      "content": "/* ============================================================================\n   Element — Pagination\n   ----------------------------------------------------------------------------\n   A row of page controls: first / previous / a window of numbers / next / last.\n   The window holds at most ten numbers and slides with the current page. There\n   is no ellipsis element, because a window that never grows past ten has\n   nothing to elide — adding one would be a promise the stylesheet cannot keep.\n\n   Every control is a <button>. They were <span>s while this lived inside the\n   Grid footer, which meant Tab never reached them; a control that changes what\n   the table shows has to be reachable without a pointer.\n\n   atom-pagination                            <nav aria-label=\"페이지\">\n     --size_{small|medium}                    medium is the default\n     __btn    one of the four edge controls — icon only, so each carries its\n              own aria-label\n     __page   one page number      [aria-current=\"page\"]\n\n   Both parts take [aria-disabled=\"true\"] and the [data-hover] documentation\n   hook.\n\n   The block carries no margin of its own. Where the row sits is the caller's\n   business — the Grid footer centres it against the Grid, a card list may leave\n   it at the inline start — and a margin here would fight both.\n   ========================================================================== */\n\n.atom-pagination {\n  display: flex;\n  align-items: center;\n  gap: 2px;\n}\n\n/* One shape for both kinds of control, so the row reads as a single strip\n   rather than icons with numbers dropped between them. min-width rather than\n   width: a four-digit page widens its own box instead of spilling out. */\n.atom-pagination__btn,\n.atom-pagination__page {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  min-width: 30px;\n  height: 30px;\n  padding-inline: 6px;\n  border-radius: var(--atom-radius-r1);\n  font-size: 13px;\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-pagination__btn svg {\n  width: 14px;\n  height: 14px;\n}\n\n/* The numbers under the cursor change as the user walks the set. tabular-nums\n   keeps 1 and 8 the same width so the row does not shift under the hand that\n   is about to press the same button again. */\n.atom-pagination__page {\n  font-variant-numeric: tabular-nums;\n}\n\n/* --- sizes ---------------------------------------------------------------- */\n/* medium is the default and needs no rule. */\n.atom-pagination--size_small .atom-pagination__btn,\n.atom-pagination--size_small .atom-pagination__page {\n  min-width: 26px;\n  height: 26px;\n  padding-inline: 5px;\n  font-size: 12px;\n}\n\n.atom-pagination--size_small .atom-pagination__btn svg {\n  width: 12px;\n  height: 12px;\n}\n\n/* --- states --------------------------------------------------------------- */\n/* `data-hover` is a documentation hook: it renders the hover treatment in a\n   demo or screenshot, where there is no pointer to produce it. */\n.atom-pagination__btn:hover,\n.atom-pagination__btn[data-hover],\n.atom-pagination__page:hover:not([aria-current=\"page\"]),\n.atom-pagination__page[data-hover]:not([aria-current=\"page\"]) {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n/* The current page is filled rather than outlined. It is the one item in the\n   row that has to read as \"you are here\" at a glance instead of as another\n   target, and a border would put it in the same family as the rest. */\n.atom-pagination__page[aria-current=\"page\"] {\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n  font-weight: 600;\n}\n\n/* The shared :focus-visible ring sits 2px outside the control, and the gap\n   between controls is 2px — so an outside ring lands on its neighbour. Pulled\n   inside, the way tabs.css does it for the same reason. */\n.atom-pagination__btn:focus-visible,\n.atom-pagination__page:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -2px;\n}\n\n/* aria-disabled, not `disabled`: the control keeps its place in the tab order,\n   so a keyboard user reaching 처음으로 on page 1 is told it is there and\n   unavailable rather than finding a hole where it was. pointer-events is what\n   stops the click. */\n.atom-pagination__btn[aria-disabled=\"true\"],\n.atom-pagination__page[aria-disabled=\"true\"] {\n  opacity: 0.35;\n  pointer-events: none;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/pagination.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/select.css",
      "content": "/* ============================================================================\n   Element — Select\n   ----------------------------------------------------------------------------\n   A trigger that opens a listbox. Options carry an optional leading icon, a\n   label, a description and a selected check.\n\n   atom-select\n     --size_{small|medium|large}             medium is the default\n     --variant_{weak|outline}                weak is the default\n     [data-state=\"closed|open\"]  [data-invalid]  [data-disabled]\n     __trigger  __value  __caret\n     __listbox  __group-label  __option  __option-icon  __option-text\n     __option-label  __option-description  __option-check\n     __empty\n   ========================================================================== */\n\n.atom-select {\n  position: relative;\n  width: 100%;\n  min-width: 0;\n}\n\n/* --- trigger ------------------------------------------------------------- */\n.atom-select__trigger {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x3);\n  width: 100%;\n  height: var(--atom-control-h);\n  padding-inline: var(--atom-x4);\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-bg-neutral-weak);\n  font-size: 15px;\n  color: var(--atom-fg-neutral);\n  text-align: left;\n  transition: box-shadow var(--atom-duration-fast) ease,\n    filter var(--atom-duration-fast) ease;\n}\n\n.atom-select__value {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.atom-select--empty .atom-select__value,\n.atom-select__trigger--empty .atom-select__value {\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-select__caret {\n  width: 24px;\n  height: 24px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-muted);\n  transition: transform var(--atom-duration-base) var(--atom-ease-out);\n}\n\n/* --- listbox ------------------------------------------------------------- */\n.atom-select__listbox {\n  margin-top: 6px;\n  padding: 6px;\n  border-radius: var(--atom-radius-r3);\n  background: var(--atom-bg-layer);\n  box-shadow: var(--atom-shadow-s2), inset 0 0 0 1px var(--atom-border-subtle);\n}\n\n.atom-select__group-label {\n  padding: 6px 10px 4px;\n  font-size: 11px;\n  font-weight: 600;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-select__option {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 9px 10px;\n  border-radius: var(--atom-radius-r2);\n  font-size: 14px;\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease;\n}\n\n.atom-select__option-icon {\n  width: 20px;\n  height: 20px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-select__option-text {\n  display: flex;\n  flex-direction: column;\n  min-width: 0;\n  flex: 1;\n}\n\n.atom-select__option-label {\n  font-weight: 500;\n  color: var(--atom-fg-neutral);\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.atom-select__option-description {\n  font-size: 12px;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-select__option-check {\n  width: 18px;\n  height: 18px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral);\n  opacity: 0;\n}\n\n.atom-select__empty {\n  padding: var(--atom-x5) 10px;\n  text-align: center;\n  font-size: 13px;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-select--size_small .atom-select__trigger {\n  height: var(--atom-control-h-small);\n  font-size: 14px;\n}\n\n.atom-select--size_small .atom-select__caret {\n  width: 24px;\n  height: 24px;\n}\n\n.atom-select--size_large .atom-select__trigger {\n  height: var(--atom-control-h-large);\n  border-radius: var(--atom-radius-r3);\n  font-size: 16px;\n}\n\n/* --- variants ------------------------------------------------------------ */\n.atom-select--variant_outline .atom-select__trigger {\n  background: var(--atom-bg-layer);\n  box-shadow: var(--atom-control-border);\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-select__trigger:hover {\n  filter: brightness(0.96);\n}\n\n/* One ring for every control — see the Control surface block in tokens.css.\n   The trigger is the focusable element here (Input hands focus to an inner\n   <input>), so base.css's outset ring is replaced rather than layered: an\n   outset ring with a 2px offset crosses the 4px gap to its neighbour when a\n   field puts this next to another control. Placed above the invalid rule so\n   invalid keeps outranking focus. */\n.atom-select__trigger:focus-visible {\n  outline: none;\n  box-shadow: var(--atom-control-border-focus);\n}\n\n.atom-select[data-state=\"open\"] .atom-select__caret {\n  transform: rotate(180deg);\n}\n\n.atom-select[data-state=\"closed\"] .atom-select__listbox {\n  display: none;\n}\n\n.atom-select__option[aria-selected=\"true\"] {\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-select__option[aria-selected=\"true\"] .atom-select__option-check {\n  opacity: 1;\n}\n\n.atom-select__option:hover:not([aria-disabled=\"true\"]) {\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-select__option[aria-disabled=\"true\"] {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n.atom-select[data-invalid] .atom-select__trigger {\n  box-shadow: var(--atom-control-border-invalid);\n}\n\n.atom-select[data-disabled] {\n  opacity: 0.4;\n  pointer-events: none;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/select.css"
    }
  ],
  "categories": [
    "navigation"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "component",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Pagination"
    ],
    "docs": "poc/design-system/components/pagination.md"
  }
}
