{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid",
  "type": "registry:ui",
  "title": "Grid",
  "description": "An interactive data grid built on a real <table role=\"grid\">: a single tab stop with 2-D arrow-key cell navigation (roving tabindex), Home/End to row ends, and Ctrl+Home/End to the grid corners.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/grid.tsx",
      "type": "registry:ui",
      "target": "components/ui/grid.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { PropsWithChildren } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// Grid — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// An INTERACTIVE data grid: a single tab stop with 2-D arrow-key cell\n// navigation (roving tabindex). This is deliberately distinct from the\n// static <Table> component — Table is the right choice for read-only\n// tabular data (every focusable link/button stays in the tab sequence and\n// AT gets native row/column navigation). Reach for Grid only when you want\n// spreadsheet-style cell focus and a SHORTER tab sequence (the whole grid\n// is one tab stop). See the APG comparison of the two patterns.\n//\n// shadcn/ui has no Grid component — this maps to the WAI-ARIA APG Grid\n// pattern, built on a real <table> so we inherit the semantic table model\n// and only layer the grid roles + roving tabindex on top.\n//\n// Refs (read before editing):\n//   repos/aria-practices/content/patterns/grid/grid-pattern.html\n//     — keyboard contract + roles/states. Arrow keys move one cell; Home/End\n//       jump to row ends; Ctrl+Home/End jump to the grid's first/last cell.\n//   repos/aria-practices/content/patterns/grid/examples/js/dataGrid.js\n//     — the reference roving-tabindex implementation we model (one cell at\n//       tabindex=\"0\", the rest at -1; setFocusPointer rolls the 0).\n//   repos/mdn/files/en-us/web/html/reference/elements/table/index.md\n//   repos/mdn/files/en-us/web/accessibility/aria/reference/roles/grid_role/index.md\n//\n// The ARIA contract:\n//   - The container is a <table role=\"grid\"> with an accessible name\n//     (aria-label or aria-labelledby). role=\"grid\" switches screen readers\n//     into application mode so the arrow-key contract is exposed.\n//   - Native <tr> carries the implicit role=\"row\"; <th scope=\"col\"> the\n//     implicit role=\"columnheader\"; <td> the implicit role=\"gridcell\". We\n//     keep the native elements and do NOT add aria-rowspan/colspan — per the\n//     APG note, a grid built from a <table> must use HTML rowspan/colspan.\n//   - Every focusable cell is marked [data-grid-cell] so the keyboard layer\n//     can build its 2-D map. Exactly one carries tabindex=\"0\"; the rest -1.\n//   - If a cell contains a single interactive widget (link/button), grid\n//     navigation focuses that widget directly (APG \"focus an element inside\n//     the cell\"); otherwise it focuses the cell itself. We expose that via\n//     <GridCell as=\"a\"> / interactive children — the cell stays the\n//     [data-grid-cell] hook and is the thing that gets tabindex.\n//   - aria-sort lives on a header cell when the column is sortable (the sort\n//     control routes through htmx, exactly like <Table>).\n//\n// A tiny inline boot <script> sets the initial roving tabindex before paint\n// (no flash of all-tabbable cells); public/site.js (keyed on\n// data-slot=\"grid\") owns the live arrow/Home/End/Ctrl+Home/End contract.\n\nexport type GridSort = \"none\" | \"ascending\" | \"descending\"\n\nconst gridBase = \"w-full caption-bottom border-separate border-spacing-0 text-sm\"\n\n// Cells get a focus ring on the cell itself (roving tabindex lands here).\nconst cellBase =\n  \"border-b border-r px-3 py-2 align-middle outline-none \" +\n  \"focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 \" +\n  // The roving-tabindex owner reads as the active cell even before focus.\n  \"data-[grid-active=true]:bg-muted/50\"\n\nconst headBase =\n  cellBase +\n  \" border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\"\n\nconst dataCellBase = cellBase + \" bg-background text-foreground\"\n\ntype GridProps = PropsWithChildren<{\n  // APG: a grid MUST have an accessible name. Provide one of these.\n  ariaLabel?: string\n  ariaLabelledby?: string\n  // Optional caption/description element id (announced after the name).\n  ariaDescribedby?: string\n  // Total counts when rows/cols are virtualised or not all in the DOM.\n  ariaRowcount?: number\n  ariaColcount?: number\n  // Editing is disabled across the whole grid (read-only data grid).\n  ariaReadonly?: boolean\n  // The grid supports selecting more than one cell/row. Pair with the\n  // `selected` props on GridRow/GridCell; selectable-but-unselected nodes\n  // should then carry aria-selected=\"false\" so AT advertises selectability.\n  // aria-multiselectable: w3c.github.io/aria/#aria-multiselectable\n  ariaMultiselectable?: boolean\n  class?: ClassValue\n  wrapperClass?: ClassValue\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n  [key: `aria-${string}`]: any\n}>\n\nexport function Grid(props: GridProps) {\n  const {\n    ariaLabel,\n    ariaLabelledby,\n    ariaDescribedby,\n    ariaRowcount,\n    ariaColcount,\n    ariaReadonly,\n    ariaMultiselectable,\n    class: className,\n    wrapperClass,\n    children,\n    ...rest\n  } = props as any\n  // Boot script: roll the roving tabindex to the FIRST focusable cell before\n  // paint, so the grid is a single tab stop immediately (no flash of every\n  // cell being tabbable). Models dataGrid.js setFocusPointer(0,0).\n  const boot = `(function(el){\n    var cells = el.querySelectorAll('[data-grid-cell]');\n    cells.forEach(function(c,i){ c.setAttribute('tabindex', i===0 ? '0' : '-1'); });\n    if (cells.length) cells[0].setAttribute('data-grid-active','true');\n    el.setAttribute('data-grid-ready','true');\n  })(document.currentScript.previousElementSibling);`\n  return (\n    <div class={cn(\"relative w-full overflow-auto rounded-md border\", wrapperClass)}>\n      <table\n        role=\"grid\"\n        data-slot=\"grid\"\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledby}\n        aria-describedby={ariaDescribedby}\n        aria-rowcount={ariaRowcount}\n        aria-colcount={ariaColcount}\n        aria-readonly={ariaReadonly ? \"true\" : undefined}\n        aria-multiselectable={ariaMultiselectable ? \"true\" : undefined}\n        class={cn(gridBase, className)}\n        {...rest}\n      >\n        {children}\n      </table>\n      <script\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: SSR boot\n        dangerouslySetInnerHTML={{ __html: boot }}\n      />\n    </div>\n  )\n}\n\nexport function GridHeader(props: PropsWithChildren<{ class?: ClassValue }>) {\n  return (\n    <thead data-slot=\"grid-header\" class={cn(props.class)}>\n      {props.children}\n    </thead>\n  )\n}\n\nexport function GridBody(props: PropsWithChildren<{ class?: ClassValue }>) {\n  return (\n    <tbody data-slot=\"grid-body\" class={cn(props.class)}>\n      {props.children}\n    </tbody>\n  )\n}\n\ntype GridRowProps = PropsWithChildren<{\n  class?: ClassValue\n  // 1-based row position when not all rows are in the DOM (virtualised).\n  ariaRowindex?: number\n  // 1-based column index of the row's first cell when the visible columns are\n  // contiguous: set aria-colindex ONCE on the row and browsers compute each\n  // cell's column number (preferred over per-cell when columns are contiguous).\n  // aria-colindex: w3c.github.io/aria/#aria-colindex\n  ariaColindex?: number\n  selected?: boolean\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n}>\n\nexport function GridRow(props: GridRowProps) {\n  const { children, class: className, ariaRowindex, ariaColindex, selected, ...rest } = props as any\n  return (\n    <tr\n      data-slot=\"grid-row\"\n      aria-rowindex={ariaRowindex}\n      aria-colindex={ariaColindex}\n      aria-selected={selected ? \"true\" : undefined}\n      class={cn(\"transition-colors\", className)}\n      {...rest}\n    >\n      {children}\n    </tr>\n  )\n}\n\ntype GridColumnHeaderProps = PropsWithChildren<{\n  class?: ClassValue\n  // Sort state. Omit for non-sortable columns.\n  sort?: GridSort\n  // 1-based column position when columns are virtualised.\n  ariaColindex?: number\n  // htmx attrs ride onto the header cell (e.g. hx-get to re-fetch sorted).\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n}>\n\n// A column header. It is focusable (a [data-grid-cell]) so screen-reader\n// users in application mode can reach it with the arrow keys — APG: header\n// cells should be focusable when they provide functions like sort.\nexport function GridColumnHeader(props: GridColumnHeaderProps) {\n  const { children, class: className, sort, ariaColindex, ...rest } = props as any\n  const sortable = sort !== undefined\n  return (\n    <th\n      scope=\"col\"\n      data-slot=\"grid-columnheader\"\n      data-grid-cell=\"\"\n      data-sortable={sortable ? \"true\" : undefined}\n      aria-sort={sortable ? sort : undefined}\n      aria-colindex={ariaColindex}\n      class={cn(headBase, className)}\n      {...rest}\n    >\n      <span class=\"inline-flex items-center gap-1.5\">\n        {children}\n        {sort === \"ascending\" && (\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\">\n            <polyline points=\"18 15 12 9 6 15\" />\n          </svg>\n        )}\n        {sort === \"descending\" && (\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\">\n            <polyline points=\"6 9 12 15 18 9\" />\n          </svg>\n        )}\n        {sort === \"none\" && (\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5 opacity-30\" aria-hidden=\"true\">\n            <polyline points=\"6 9 12 15 18 9\" />\n          </svg>\n        )}\n      </span>\n    </th>\n  )\n}\n\ntype GridRowHeaderProps = PropsWithChildren<{\n  class?: ClassValue\n  ariaColindex?: number\n  [key: `data-${string}`]: any\n}>\n\n// A row header (scope=\"row\") — title information for the row, focusable.\nexport function GridRowHeader(props: GridRowHeaderProps) {\n  const { children, class: className, ariaColindex, ...rest } = props as any\n  return (\n    <th\n      scope=\"row\"\n      data-slot=\"grid-rowheader\"\n      data-grid-cell=\"\"\n      aria-colindex={ariaColindex}\n      class={cn(headBase, \"font-medium text-foreground\", className)}\n      {...rest}\n    >\n      {children}\n    </th>\n  )\n}\n\ntype GridCellProps = PropsWithChildren<{\n  class?: ClassValue\n  ariaColindex?: number\n  selected?: boolean\n  // Editing disabled for this specific cell.\n  ariaReadonly?: boolean\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n}>\n\nexport function GridCell(props: GridCellProps) {\n  const { children, class: className, ariaColindex, selected, ariaReadonly, ...rest } =\n    props as any\n  return (\n    <td\n      data-slot=\"grid-cell\"\n      data-grid-cell=\"\"\n      aria-colindex={ariaColindex}\n      aria-selected={selected ? \"true\" : undefined}\n      aria-readonly={ariaReadonly ? \"true\" : undefined}\n      class={cn(dataCellBase, className)}\n      {...rest}\n    >\n      {children}\n    </td>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/grid.html",
      "type": "registry:file",
      "target": "templates/components/grid.html",
      "content": "{# Grid macros — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/grid.tsx. An INTERACTIVE data grid: <table role=\"grid\">\n   that is a SINGLE tab stop with 2-D arrow-key cell navigation (roving\n   tabindex). Distinct from the static Table component.\n\n   The boot <script> emitted by grid_close() sets the roving tabindex on\n   first paint (the first focusable [data-grid-cell] gets tabindex=\"0\", the\n   rest -1); public/site.js (keyed on data-slot=\"grid\") owns the live\n   arrow / Home / End / Ctrl+Home / Ctrl+End contract.\n\n   Accessibility contract:\n     repos/aria-practices/content/patterns/grid/grid-pattern.html\n     repos/aria-practices/content/patterns/grid/examples/js/dataGrid.js\n\n   Usage:\n     {% from \"components/grid.html\" import grid_open, grid_close,\n          ghead_open, ghead_close, gbody_open, gbody_close,\n          grow_open, grow_close, gcolheader, growheader, gcell %}\n\n     {{ grid_open(aria_label=\"Transactions\") }}\n       {{ ghead_open() }}{{ grow_open() }}\n         {{ gcolheader(\"Name\", sort=\"ascending\") }}{{ gcolheader(\"Amount\") }}\n       {{ grow_close() }}{{ ghead_close() }}\n       {{ gbody_open() }}{{ grow_open() }}\n         {{ gcell(\"Ada\") }}{{ gcell(\"$120\") }}\n       {{ grow_close() }}{{ gbody_close() }}\n     {{ grid_close() }} #}\n\n{% macro grid_open(aria_label=none, aria_labelledby=none, aria_describedby=none, aria_rowcount=none, aria_colcount=none, aria_readonly=false, aria_multiselectable=false, extra_class=\"\", wrapper_class=\"\", attrs={}) -%}\n<div class=\"relative w-full overflow-auto rounded-md border {{ wrapper_class }}\">\n  <table role=\"grid\"\n         data-slot=\"grid\"\n         {%- if aria_label %} aria-label=\"{{ aria_label }}\"{% endif %}\n         {%- if aria_labelledby %} aria-labelledby=\"{{ aria_labelledby }}\"{% endif %}\n         {%- if aria_describedby %} aria-describedby=\"{{ aria_describedby }}\"{% endif %}\n         {%- if aria_rowcount is not none %} aria-rowcount=\"{{ aria_rowcount }}\"{% endif %}\n         {%- if aria_colcount is not none %} aria-colcount=\"{{ aria_colcount }}\"{% endif %}\n         {%- if aria_readonly %} aria-readonly=\"true\"{% endif %}\n         {#- grid supports multi-selection: w3c.github.io/aria/#aria-multiselectable -#}\n         {%- if aria_multiselectable %} aria-multiselectable=\"true\"{% endif %}\n         {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}\n         class=\"w-full caption-bottom border-separate border-spacing-0 text-sm {{ extra_class }}\">\n{%- endmacro %}\n\n{% macro grid_close() -%}\n  </table>\n  <script>(function(el){\n    var cells = el.querySelectorAll('[data-grid-cell]');\n    cells.forEach(function(c,i){ c.setAttribute('tabindex', i===0 ? '0' : '-1'); });\n    if (cells.length) cells[0].setAttribute('data-grid-active','true');\n    el.setAttribute('data-grid-ready','true');\n  })(document.currentScript.previousElementSibling);</script>\n</div>\n{%- endmacro %}\n\n{% macro ghead_open() %}<thead data-slot=\"grid-header\">{% endmacro %}\n{% macro ghead_close() %}</thead>{% endmacro %}\n\n{% macro gbody_open() %}<tbody data-slot=\"grid-body\">{% endmacro %}\n{% macro gbody_close() %}</tbody>{% endmacro %}\n\n{% macro grow_open(aria_rowindex=none, aria_colindex=none, selected=false, extra_class=\"\", attrs={}) -%}\n<tr data-slot=\"grid-row\"\n    {%- if aria_rowindex is not none %} aria-rowindex=\"{{ aria_rowindex }}\"{% endif %}\n    {#- contiguous-columns form: aria-colindex once on the row. w3c.github.io/aria/#aria-colindex -#}\n    {%- if aria_colindex is not none %} aria-colindex=\"{{ aria_colindex }}\"{% endif %}\n    {%- if selected %} aria-selected=\"true\"{% endif %}\n    {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}\n    class=\"transition-colors {{ extra_class }}\">\n{%- endmacro %}\n{% macro grow_close() %}</tr>{% endmacro %}\n\n{%- set CELL = \"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50\" -%}\n{%- set HEAD = CELL ~ \" border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\" -%}\n{%- set DATA = CELL ~ \" bg-background text-foreground\" -%}\n\n{% macro gcolheader(label, sort=none, aria_colindex=none, extra_class=\"\", attrs={}) -%}\n<th scope=\"col\"\n    data-slot=\"grid-columnheader\"\n    data-grid-cell=\"\"\n    {%- if sort is not none %} data-sortable=\"true\" aria-sort=\"{{ sort }}\"{% endif %}\n    {%- if aria_colindex is not none %} aria-colindex=\"{{ aria_colindex }}\"{% endif %}\n    {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}\n    class=\"{{ HEAD }} {{ extra_class }}\"><span class=\"inline-flex items-center gap-1.5\">{{ label|safe }}{% if sort == \"ascending\" %}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\"><polyline points=\"18 15 12 9 6 15\" /></svg>{% elif sort == \"descending\" %}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>{% elif sort == \"none\" %}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5 opacity-30\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>{% endif %}</span></th>\n{%- endmacro %}\n\n{% macro growheader(label, aria_colindex=none, extra_class=\"\", attrs={}) -%}\n<th scope=\"row\"\n    data-slot=\"grid-rowheader\"\n    data-grid-cell=\"\"\n    {%- if aria_colindex is not none %} aria-colindex=\"{{ aria_colindex }}\"{% endif %}\n    {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}\n    class=\"{{ HEAD }} font-medium text-foreground {{ extra_class }}\">{{ label|safe }}</th>\n{%- endmacro %}\n\n{% macro gcell(content, aria_colindex=none, selected=false, aria_readonly=false, extra_class=\"\", attrs={}) -%}\n<td data-slot=\"grid-cell\"\n    data-grid-cell=\"\"\n    {%- if aria_colindex is not none %} aria-colindex=\"{{ aria_colindex }}\"{% endif %}\n    {%- if selected %} aria-selected=\"true\"{% endif %}\n    {%- if aria_readonly %} aria-readonly=\"true\"{% endif %}\n    {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}\n    class=\"{{ DATA }} {{ extra_class }}\">{{ content|safe }}</td>\n{%- endmacro %}\n"
    },
    {
      "path": "registry/go-templates/grid.tmpl",
      "type": "registry:file",
      "target": "components/grid.tmpl",
      "content": "{{/*\n  Grid templates — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/grid.tsx. An INTERACTIVE data grid: <table role=\"grid\">\n  that is a SINGLE tab stop with 2-D arrow-key cell navigation (roving\n  tabindex). Distinct from the static \"table\" component.\n\n  Named templates:\n    - \"grid\"          — wrapper + <table role=\"grid\"> + boot script (pass .Body)\n    - \"grid_row\"      — one <tr> (pass .Body HTML)\n    - \"grid_colheader\" — a focusable <th scope=\"col\"> (optional .Sort)\n    - \"grid_rowheader\" — a focusable <th scope=\"row\">\n    - \"grid_cell\"     — a focusable <td role=\"gridcell\"> (pass .Body)\n\n  The boot script sets the roving tabindex (single tab stop) on first paint;\n  public/site.js (keyed on data-slot=\"grid\") owns the arrow / Home / End /\n  Ctrl+Home / Ctrl+End contract.\n\n  Accessibility contract:\n    repos/aria-practices/content/patterns/grid/grid-pattern.html\n    repos/aria-practices/content/patterns/grid/examples/js/dataGrid.js\n\n  Hand-compose the inner HTML (rows of headers/cells), then pass it as .Body\n  (template.HTML via htmlSafe).\n*/}}\n\n{{define \"grid\"}}\n<div class=\"relative w-full overflow-auto rounded-md border\">\n  <table role=\"grid\"\n         data-slot=\"grid\"\n         {{- if .AriaLabel}} aria-label=\"{{.AriaLabel}}\"{{end}}\n         {{- if .AriaLabelledby}} aria-labelledby=\"{{.AriaLabelledby}}\"{{end}}\n         {{- if .AriaDescribedby}} aria-describedby=\"{{.AriaDescribedby}}\"{{end}}\n         {{- if .AriaRowcount}} aria-rowcount=\"{{.AriaRowcount}}\"{{end}}\n         {{- if .AriaColcount}} aria-colcount=\"{{.AriaColcount}}\"{{end}}\n         {{- if .AriaReadonly}} aria-readonly=\"true\"{{end}}\n         {{/* grid supports multi-selection: w3c.github.io/aria/#aria-multiselectable */}}\n         {{- if .AriaMultiselectable}} aria-multiselectable=\"true\"{{end}}\n         class=\"w-full caption-bottom border-separate border-spacing-0 text-sm\">\n    {{.Body}}\n  </table>\n  <script>(function(el){\n    var cells = el.querySelectorAll('[data-grid-cell]');\n    cells.forEach(function(c,i){ c.setAttribute('tabindex', i===0 ? '0' : '-1'); });\n    if (cells.length) cells[0].setAttribute('data-grid-active','true');\n    el.setAttribute('data-grid-ready','true');\n  })(document.currentScript.previousElementSibling);</script>\n</div>\n{{end}}\n\n{{define \"grid_row\"}}\n<tr data-slot=\"grid-row\"\n    {{- if .AriaRowindex}} aria-rowindex=\"{{.AriaRowindex}}\"{{end}}\n    {{/* contiguous-columns form: aria-colindex once on the row. w3c.github.io/aria/#aria-colindex */}}\n    {{- if .AriaColindex}} aria-colindex=\"{{.AriaColindex}}\"{{end}}\n    {{- if .Selected}} aria-selected=\"true\"{{end}}\n    class=\"transition-colors\">{{.Body}}</tr>\n{{end}}\n\n{{define \"grid_colheader\"}}\n{{- $cell := \"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50\" -}}\n{{- $head := printf \"%s border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\" $cell -}}\n<th scope=\"col\"\n    data-slot=\"grid-columnheader\"\n    data-grid-cell=\"\"\n    {{- if .Sort}} data-sortable=\"true\" aria-sort=\"{{.Sort}}\"{{end}}\n    {{- if .AriaColindex}} aria-colindex=\"{{.AriaColindex}}\"{{end}}\n    class=\"{{$head}}\"><span class=\"inline-flex items-center gap-1.5\">{{.Label}}{{if eq .Sort \"ascending\"}}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\"><polyline points=\"18 15 12 9 6 15\"/></svg>{{else if eq .Sort \"descending\"}}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\"/></svg>{{else if eq .Sort \"none\"}}<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5 opacity-30\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\"/></svg>{{end}}</span></th>\n{{end}}\n\n{{define \"grid_rowheader\"}}\n{{- $cell := \"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50\" -}}\n{{- $head := printf \"%s border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md font-medium text-foreground\" $cell -}}\n<th scope=\"row\"\n    data-slot=\"grid-rowheader\"\n    data-grid-cell=\"\"\n    {{- if .AriaColindex}} aria-colindex=\"{{.AriaColindex}}\"{{end}}\n    class=\"{{$head}}\">{{.Label}}</th>\n{{end}}\n\n{{define \"grid_cell\"}}\n{{- $cell := \"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50\" -}}\n{{- $data := printf \"%s bg-background text-foreground\" $cell -}}\n<td data-slot=\"grid-cell\"\n    data-grid-cell=\"\"\n    {{- if .AriaColindex}} aria-colindex=\"{{.AriaColindex}}\"{{end}}\n    {{- if .Selected}} aria-selected=\"true\"{{end}}\n    {{- if .AriaReadonly}} aria-readonly=\"true\"{{end}}\n    class=\"{{$data}}\">{{.Body}}</td>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/grid.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/grid.ex",
      "content": "defmodule ShadcnHtmx.Components.Grid do\n  @moduledoc \"\"\"\n  Grid — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  An INTERACTIVE data grid: a `<table role=\"grid\">` that is a SINGLE tab stop\n  with 2-D arrow-key cell navigation (roving tabindex). Distinct from the\n  static `table` component — reach for `grid` only when you want\n  spreadsheet-style cell focus and a shorter tab sequence.\n\n  Mirrors registry/ui/grid.tsx. Function components: `grid`, `grid_header`,\n  `grid_body`, `grid_row`, `grid_columnheader`, `grid_rowheader`, `grid_cell`.\n\n  A boot `<script>` sets the roving tabindex on first paint, and\n  public/site.js (keyed on data-slot=\"grid\") owns the arrow / Home / End /\n  Ctrl+Home / Ctrl+End contract. Accessibility contract:\n  repos/aria-practices/content/patterns/grid/grid-pattern.html\n  repos/aria-practices/content/patterns/grid/examples/js/dataGrid.js\n\n  ## Examples\n\n      <.grid aria-label=\"Transactions\">\n        <.grid_header>\n          <.grid_row>\n            <.grid_columnheader sort=\"ascending\">Name</.grid_columnheader>\n            <.grid_columnheader>Amount</.grid_columnheader>\n          </.grid_row>\n        </.grid_header>\n        <.grid_body>\n          <.grid_row>\n            <.grid_cell>Ada</.grid_cell>\n            <.grid_cell>$120</.grid_cell>\n          </.grid_row>\n        </.grid_body>\n      </.grid>\n  \"\"\"\n\n  use Phoenix.Component\n\n  @cell \"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50\"\n  @head @cell <>\n          \" border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\"\n  @data @cell <> \" bg-background text-foreground\"\n\n  attr :\"aria-label\", :string, default: nil\n  attr :\"aria-labelledby\", :string, default: nil\n  attr :\"aria-describedby\", :string, default: nil\n  attr :\"aria-rowcount\", :integer, default: nil\n  attr :\"aria-colcount\", :integer, default: nil\n  attr :\"aria-readonly\", :boolean, default: false\n  # grid supports selecting more than one cell/row: w3c.github.io/aria/#aria-multiselectable\n  attr :\"aria-multiselectable\", :boolean, default: false\n  attr :class, :string, default: nil\n  attr :wrapper_class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def grid(assigns) do\n    ~H\"\"\"\n    <div class={[\"relative w-full overflow-auto rounded-md border\", @wrapper_class]}>\n      <table\n        role=\"grid\"\n        data-slot=\"grid\"\n        aria-label={assigns[:\"aria-label\"]}\n        aria-labelledby={assigns[:\"aria-labelledby\"]}\n        aria-describedby={assigns[:\"aria-describedby\"]}\n        aria-rowcount={assigns[:\"aria-rowcount\"]}\n        aria-colcount={assigns[:\"aria-colcount\"]}\n        aria-readonly={assigns[:\"aria-readonly\"] && \"true\"}\n        aria-multiselectable={assigns[:\"aria-multiselectable\"] && \"true\"}\n        class={[\"w-full caption-bottom border-separate border-spacing-0 text-sm\", @class]}\n        {@rest}\n      >\n        {render_slot(@inner_block)}\n      </table>\n      <script>{Phoenix.HTML.raw(~s\"\"\"\n        (function(el){\n          var cells = el.querySelectorAll('[data-grid-cell]');\n          cells.forEach(function(c,i){ c.setAttribute('tabindex', i===0 ? '0' : '-1'); });\n          if (cells.length) cells[0].setAttribute('data-grid-active','true');\n          el.setAttribute('data-grid-ready','true');\n        })(document.currentScript.previousElementSibling);\n      \"\"\")}</script>\n    </div>\n    \"\"\"\n  end\n\n  slot :inner_block, required: true\n  def grid_header(assigns), do: ~H\"<thead data-slot=\\\"grid-header\\\">{render_slot(@inner_block)}</thead>\"\n\n  slot :inner_block, required: true\n  def grid_body(assigns), do: ~H\"<tbody data-slot=\\\"grid-body\\\">{render_slot(@inner_block)}</tbody>\"\n\n  attr :\"aria-rowindex\", :integer, default: nil\n  # contiguous-columns form: aria-colindex once on the row. w3c.github.io/aria/#aria-colindex\n  attr :\"aria-colindex\", :integer, default: nil\n  attr :selected, :boolean, default: false\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def grid_row(assigns) do\n    ~H\"\"\"\n    <tr\n      data-slot=\"grid-row\"\n      aria-rowindex={assigns[:\"aria-rowindex\"]}\n      aria-colindex={assigns[:\"aria-colindex\"]}\n      aria-selected={@selected && \"true\"}\n      class={[\"transition-colors\", @class]}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </tr>\n    \"\"\"\n  end\n\n  attr :sort, :string, default: nil, values: [nil, \"none\", \"ascending\", \"descending\"]\n  attr :\"aria-colindex\", :integer, default: nil\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def grid_columnheader(assigns) do\n    assigns = assign(assigns, head: @head)\n\n    ~H\"\"\"\n    <th\n      scope=\"col\"\n      data-slot=\"grid-columnheader\"\n      data-grid-cell=\"\"\n      data-sortable={@sort && \"true\"}\n      aria-sort={@sort}\n      aria-colindex={assigns[:\"aria-colindex\"]}\n      class={[@head, @class]}\n      {@rest}\n    >\n      <span class=\"inline-flex items-center gap-1.5\">\n        {render_slot(@inner_block)}\n        <svg :if={@sort == \"ascending\"} xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\">\n          <polyline points=\"18 15 12 9 6 15\" />\n        </svg>\n        <svg :if={@sort == \"descending\"} xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\">\n          <polyline points=\"6 9 12 15 18 9\" />\n        </svg>\n        <svg :if={@sort == \"none\"} xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5 opacity-30\" aria-hidden=\"true\">\n          <polyline points=\"6 9 12 15 18 9\" />\n        </svg>\n      </span>\n    </th>\n    \"\"\"\n  end\n\n  attr :\"aria-colindex\", :integer, default: nil\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def grid_rowheader(assigns) do\n    assigns = assign(assigns, head: @head)\n\n    ~H\"\"\"\n    <th\n      scope=\"row\"\n      data-slot=\"grid-rowheader\"\n      data-grid-cell=\"\"\n      aria-colindex={assigns[:\"aria-colindex\"]}\n      class={[@head, \"font-medium text-foreground\", @class]}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </th>\n    \"\"\"\n  end\n\n  attr :\"aria-colindex\", :integer, default: nil\n  attr :selected, :boolean, default: false\n  attr :\"aria-readonly\", :boolean, default: false\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def grid_cell(assigns) do\n    assigns = assign(assigns, data: @data)\n\n    ~H\"\"\"\n    <td\n      data-slot=\"grid-cell\"\n      data-grid-cell=\"\"\n      aria-colindex={assigns[:\"aria-colindex\"]}\n      aria-selected={@selected && \"true\"}\n      aria-readonly={assigns[:\"aria-readonly\"] && \"true\"}\n      class={[@data, @class]}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </td>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/grid.html",
      "type": "registry:file",
      "target": "snippets/grid.html",
      "content": "<!--\n  shadcn-htmx — raw HTML grid snippet.\n\n  Mirrors registry/ui/grid.tsx. An INTERACTIVE data grid: <table role=\"grid\">\n  that is a SINGLE tab stop with 2-D arrow-key cell navigation (roving\n  tabindex). Distinct from the static <table> snippet — use this only when you\n  want spreadsheet-style cell focus.\n\n  Every focusable cell carries [data-grid-cell]; exactly one has tabindex=\"0\",\n  the rest tabindex=\"-1\". The inline <script> right after the grid sets that\n  on first paint. The full keyboard contract (Arrow keys move one cell,\n  Home/End jump to row ends, Ctrl+Home/End jump to the grid corners) needs the\n  wiring in public/site.js.\n\n  Accessibility contract:\n    repos/aria-practices/content/patterns/grid/grid-pattern.html\n\n  Optional ARIA attributes (additive, omit unless needed):\n    - On <table role=\"grid\">: add aria-multiselectable=\"true\" when the grid\n      supports selecting more than one cell/row (pair with aria-selected on\n      cells/rows; selectable-but-unselected nodes carry aria-selected=\"false\").\n      w3c.github.io/aria/#aria-multiselectable\n    - On <tr data-slot=\"grid-row\">: when the visible columns are contiguous you\n      may set aria-colindex once on the row (the row's first column index)\n      instead of per cell. w3c.github.io/aria/#aria-colindex\n\n  Required CSS theme variables: --background, --foreground, --muted,\n  --muted-foreground, --border, --ring. See app/styles/input.css.\n\n  Minimal inline JS for keyboard navigation (if you are not loading site.js):\n\n    <script>\n      document.addEventListener('keydown', function (e) {\n        var cell = e.target.closest('[data-grid-cell]')\n        if (!cell) return\n        var grid = cell.closest('[data-slot=\"grid\"]')\n        if (!grid) return\n        var keys = ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Home','End']\n        if (keys.indexOf(e.key) === -1) return\n        e.preventDefault()\n        var rows = [].slice.call(grid.querySelectorAll('tr'))\n        var grid2d = rows.map(function (r) {\n          return [].slice.call(r.querySelectorAll('[data-grid-cell]'))\n        }).filter(function (r) { return r.length })\n        var R = grid2d.length, C = grid2d[0].length\n        var r = -1, c = -1\n        grid2d.forEach(function (row, ri) {\n          var ci = row.indexOf(cell)\n          if (ci !== -1) { r = ri; c = ci }\n        })\n        if (r === -1) return\n        if (e.key === 'ArrowUp') r = Math.max(0, r - 1)\n        else if (e.key === 'ArrowDown') r = Math.min(R - 1, r + 1)\n        else if (e.key === 'ArrowLeft') c = Math.max(0, c - 1)\n        else if (e.key === 'ArrowRight') c = Math.min(C - 1, c + 1)\n        else if (e.key === 'Home') { c = 0; if (e.ctrlKey) r = 0 }\n        else if (e.key === 'End') { c = grid2d[r].length - 1; if (e.ctrlKey) r = R - 1 }\n        var next = grid2d[r][c]\n        if (!next) return\n        cell.setAttribute('tabindex', '-1'); cell.removeAttribute('data-grid-active')\n        next.setAttribute('tabindex', '0'); next.setAttribute('data-grid-active', 'true')\n        next.focus()\n      })\n    </script>\n-->\n\n<div class=\"relative w-full overflow-auto rounded-md border\">\n  <table role=\"grid\" data-slot=\"grid\" aria-label=\"Transactions\"\n         class=\"w-full caption-bottom border-separate border-spacing-0 text-sm\">\n    <thead data-slot=\"grid-header\">\n      <tr data-slot=\"grid-row\" class=\"transition-colors\">\n        <th scope=\"col\" data-slot=\"grid-columnheader\" data-grid-cell=\"\" data-sortable=\"true\" aria-sort=\"ascending\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\">\n          <span class=\"inline-flex items-center gap-1.5\">Name\n            <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-3.5\" aria-hidden=\"true\"><polyline points=\"18 15 12 9 6 15\" /></svg>\n          </span>\n        </th>\n        <th scope=\"col\" data-slot=\"grid-columnheader\" data-grid-cell=\"\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md\">\n          <span class=\"inline-flex items-center gap-1.5\">Amount</span>\n        </th>\n      </tr>\n    </thead>\n    <tbody data-slot=\"grid-body\">\n      <tr data-slot=\"grid-row\" class=\"transition-colors\">\n        <th scope=\"row\" data-slot=\"grid-rowheader\" data-grid-cell=\"\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md font-medium text-foreground\">Ada Lovelace</th>\n        <td data-slot=\"grid-cell\" data-grid-cell=\"\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 bg-background text-foreground\">$120.00</td>\n      </tr>\n      <tr data-slot=\"grid-row\" class=\"transition-colors\">\n        <th scope=\"row\" data-slot=\"grid-rowheader\" data-grid-cell=\"\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 border-t bg-muted/50 text-left font-medium text-muted-foreground first:rounded-tl-md last:rounded-tr-md font-medium text-foreground\">Grace Hopper</th>\n        <td data-slot=\"grid-cell\" data-grid-cell=\"\"\n            class=\"border-b border-r px-3 py-2 align-middle outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-[2px] focus-visible:ring-ring/50 data-[grid-active=true]:bg-muted/50 bg-background text-foreground\">$87.50</td>\n      </tr>\n    </tbody>\n  </table>\n  <script>\n    (function (el) {\n      var cells = el.querySelectorAll('[data-grid-cell]')\n      cells.forEach(function (c, i) { c.setAttribute('tabindex', i === 0 ? '0' : '-1') })\n      if (cells.length) cells[0].setAttribute('data-grid-active', 'true')\n      el.setAttribute('data-grid-ready', 'true')\n    })(document.currentScript.previousElementSibling)\n  </script>\n</div>\n"
    }
  ]
}
