{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "ledger-tables",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "The sortable, draggable header cell.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport type { SortDirection } from \"@/lib/ledger/table-sort\";\nimport { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from \"@/components/ledger/icons\";\n\nexport interface ColumnResizerProps {\n  active?: boolean;\n  className?: string;\n  /** Named in the handle's accessible label — \"Resize Account column\". */\n  label: string;\n  max?: number;\n  min?: number;\n  onKeyDown: (event: React.KeyboardEvent) => void;\n  onPointerDown: (event: React.PointerEvent) => void;\n  onReset: () => void;\n  width?: number;\n}\n\n/**\n * The 8px seam between two columns.\n *\n * It is a real `separator` with `aria-valuenow`, not a decorative div: a\n * column width that can only be changed by dragging is a feature that does not\n * exist for anyone driving the table from the keyboard. Arrows move it 16px,\n * `Alt`+arrows 1px, `Home` restores the shipped width — and so does a\n * double-click, which is the gesture every spreadsheet has trained for it.\n *\n * The hit area is 9px wide and centred on the seam; the line inside it is 1px\n * and only paints on hover, focus or during the drag. A permanently visible\n * grabber on every column boundary turns a ledger into a grid of handles.\n */\nexport const ColumnResizer = React.forwardRef<HTMLDivElement, ColumnResizerProps>(\n  ({ label, width, min, max, active, onPointerDown, onKeyDown, onReset, className }, ref) => (\n    <div\n      aria-label={`Resize ${label} column`}\n      aria-orientation=\"vertical\"\n      aria-valuemax={max}\n      aria-valuemin={min}\n      aria-valuenow={width}\n      className={cn(\"ledger-col-resizer\", className)}\n      data-resizing={active || undefined}\n      /* The header itself may be a sort button; a drag that starts on the seam\n         must not also register as a click on it. */\n      onClick={(e) => e.stopPropagation()}\n      onDoubleClick={(e) => {\n        e.preventDefault();\n        onReset();\n      }}\n      onKeyDown={onKeyDown}\n      onPointerDown={onPointerDown}\n      ref={ref}\n      role=\"separator\"\n      tabIndex={0}\n    />\n  ),\n);\nColumnResizer.displayName = \"ColumnResizer\";\n\nexport interface ColumnHeaderProps extends Omit<React.ComponentPropsWithoutRef<\"th\">, \"onSelect\"> {\n  /** Right for figures, left for everything else. */\n  align?: \"left\" | \"right\";\n  /** The column id the sort and width state is keyed by. */\n  column: string;\n  /** Set when this column is the sorted one. */\n  direction?: SortDirection;\n  onSort?: (column: string) => void;\n  /** Omit to render a header that cannot be dragged. */\n  resizer?: Omit<ColumnResizerProps, \"label\">;\n  sortable?: boolean;\n}\n\n/**\n * A `<th>` that sorts and resizes, for the tables that render their own rows.\n *\n * Server-rendered tables get the same two behaviours from `TableColumns`,\n * which reads the DOM instead — the split exists because a 400-row statement\n * should not ship 400 rows of JavaScript to be rendered twice.\n *\n * The sort control is a real `<button>` inside the cell rather than a click\n * handler on the `<th>`: the header has to be reachable and operable by\n * keyboard, and the resize handle beside it needs its own separate focus stop.\n * `aria-sort` lives on the `<th>`, where a screen reader looks for it.\n *\n * The direction indicator is always occupying its space — a chevron that\n * appears on sort would shift the label sideways on every click, and the\n * neutral state earns its glyph anyway by saying \"this column sorts\".\n */\nexport const ColumnHeader = React.forwardRef<HTMLTableCellElement, ColumnHeaderProps>(\n  (\n    {\n      column,\n      align = \"left\",\n      sortable = false,\n      direction,\n      onSort,\n      resizer,\n      className,\n      children,\n      style,\n      ...props\n    },\n    ref,\n  ) => {\n    const label = typeof children === \"string\" ? children : column;\n    const Indicator =\n      direction === \"asc\" ? ChevronUpIcon : direction === \"desc\" ? ChevronDownIcon : ChevronUpDownIcon;\n\n    return (\n      <th\n        aria-sort={\n          sortable\n            ? direction === \"asc\"\n              ? \"ascending\"\n              : direction === \"desc\"\n                ? \"descending\"\n                : \"none\"\n            : undefined\n        }\n        className={cn(\n          \"relative px-3 py-2 text-label font-medium text-text-secondary\",\n          align === \"right\" ? \"text-right\" : \"text-left\",\n          className,\n        )}\n        data-sorted={direction ?? undefined}\n        ref={ref}\n        scope=\"col\"\n        style={style}\n        {...props}\n      >\n        {sortable && onSort ? (\n          <button\n            className={cn(\n              \"group inline-flex max-w-full items-center gap-1 rounded-xs text-label font-medium\",\n              \"transition-colors duration-1 ease-standard hover:text-text-primary\",\n              \"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--accent)]\",\n              direction && \"text-text-primary\",\n              align === \"right\" && \"flex-row-reverse\",\n            )}\n            onClick={() => onSort(column)}\n            type=\"button\"\n          >\n            <span className=\"truncate\">{children}</span>\n            <Indicator\n              aria-hidden=\"true\"\n              className={cn(\n                \"size-3.5 shrink-0\",\n                direction\n                  ? \"text-accent\"\n                  : \"text-text-tertiary opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100\",\n              )}\n            />\n          </button>\n        ) : (\n          <span className=\"block truncate\">{children}</span>\n        )}\n        {resizer ? <ColumnResizer label={label} {...resizer} /> : null}\n      </th>\n    );\n  },\n);\nColumnHeader.displayName = \"ColumnHeader\";\n",
      "path": "components/ledger/column-header.tsx",
      "target": "components/ledger/column-header.tsx",
      "type": "registry:component"
    }
  ],
  "name": "column-header",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/icons",
    "@lopifi/table-sort"
  ],
  "title": "ColumnHeader",
  "type": "registry:component"
}
