{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-sortable",
  "title": "Sortable table",
  "description": "Styled table: the Sortable set. 5 decorative table variations (SortTable, ArrowsTable, IndicatorTable, MultiTable, StickyTable) on the ai2 token system, driven by CSS token transitions and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/table-sortable.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDown, ArrowDownUp, ArrowUp, ChevronDown, ChevronUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Sortable table family: 5 tables with real working sorting. Clicking the <button>\n   in the header cell GENUINELY reorders the rows; the <th> carries aria-sort. The\n   comparator is deterministic: numeric if both values are numbers, otherwise code\n   point order (NO localeCompare - no locale surprises), with a stable tie-break on\n   the original index when equal. Color comes ONLY from semantic tokens, via alpha\n   color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype Column = { key: string; label: React.ReactNode }\ntype Row = Record<string, React.ReactNode>\n\ntype TableProps = {\n  className?: string\n  size?: StyledSize\n  columns?: Column[]\n  rows?: Row[]\n}\n\n/* A sensible default data set so it renders without props. */\nconst defaultColumns: Column[] = [\n  { key: \"name\", label: \"Name\" },\n  { key: \"role\", label: \"Role\" },\n  { key: \"status\", label: \"Status\" },\n]\n\nconst defaultRows: Row[] = [\n  { name: \"Ada Lovelace\", role: \"Engineer\", status: \"Active\" },\n  { name: \"Alan Turing\", role: \"Researcher\", status: \"Active\" },\n  { name: \"Grace Hopper\", role: \"Architect\", status: \"Away\" },\n  { name: \"Katherine Johnson\", role: \"Analyst\", status: \"Active\" },\n]\n\n/* Hucre yogunlugu: padding + metin olcegi. */\nconst cell: Record<StyledSize, string> = {\n  sm: \"px-2.5 py-1.5 text-xs\",\n  md: \"px-3 py-2 text-sm\",\n  lg: \"px-4 py-2.5 text-sm\",\n  xl: \"px-5 py-3 text-base\",\n}\n\nconst headCell: Record<StyledSize, string> = {\n  sm: \"px-2.5 py-1.5 text-xs\",\n  md: \"px-3 py-2 text-xs\",\n  lg: \"px-4 py-2.5 text-sm\",\n  xl: \"px-5 py-3 text-sm\",\n}\n\nconst tableBase = \"w-full border-collapse text-left align-middle text-foreground\"\nconst headBase = \"font-medium text-muted-foreground\"\n\nconst sortBtn =\n  \"inline-flex w-full items-center gap-1.5 rounded-sm text-left outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-3.5 [&_svg]:shrink-0 [&>svg]:shrink-0 [&_i]:text-xs [&_i]:leading-none [&>i]:shrink-0\"\n\nfunction resolve(columns?: Column[], rows?: Row[]) {\n  return {\n    cols: columns && columns.length > 0 ? columns : defaultColumns,\n    data: rows && rows.length > 0 ? rows : defaultRows,\n  }\n}\n\n/* Hucre degerinden karsilastirilabilir metin. ReactNode elemanlari bos sayilir. */\nfunction toText(value: React.ReactNode): string {\n  if (typeof value === \"string\") return value\n  if (typeof value === \"number\") return String(value)\n  return \"\"\n}\n\n/* Deterministik karsilastirici: sayisal ya da kod-noktasi sirasi. */\nfunction compareText(a: string, b: string): number {\n  const na = Number(a)\n  const nb = Number(b)\n  if (a.trim() !== \"\" && b.trim() !== \"\" && !Number.isNaN(na) && !Number.isNaN(nb)) {\n    return na === nb ? 0 : na < nb ? -1 : 1\n  }\n  if (a === b) return 0\n  return a < b ? -1 : 1\n}\n\ntype Dir = \"asc\" | \"desc\"\ntype SortEntry = { key: string; dir: Dir }\n\n/* Kararli siralama: esitlikte orijinal indeks korunur. */\nfunction sortRows(data: Row[], sorts: SortEntry[]): Row[] {\n  if (sorts.length === 0) return data\n  return data\n    .map((row, index) => ({ row, index }))\n    .sort((a, b) => {\n      for (const s of sorts) {\n        const r = compareText(toText(a.row[s.key]), toText(b.row[s.key]))\n        if (r !== 0) return s.dir === \"asc\" ? r : -r\n      }\n      return a.index - b.index\n    })\n    .map((e) => e.row)\n}\n\n/* Tek kolonlu siralama durumu (asc -> desc -> asc). */\nfunction useSingleSort() {\n  const [sort, setSort] = React.useState<SortEntry | null>(null)\n  const toggle = React.useCallback((key: string) => {\n    setSort((prev) =>\n      prev && prev.key === key\n        ? { key, dir: prev.dir === \"asc\" ? \"desc\" : \"asc\" }\n        : { key, dir: \"asc\" }\n    )\n  }, [])\n  const sorts = React.useMemo(() => (sort ? [sort] : []), [sort])\n  const dirOf = React.useCallback(\n    (key: string): Dir | null => (sort && sort.key === key ? sort.dir : null),\n    [sort]\n  )\n  return { sorts, toggle, dirOf }\n}\n\n/* Cok kolonlu siralama durumu: tiklanan kolon one alinir, asc -> desc -> cikar. */\nfunction useMultiSort() {\n  const [sorts, setSorts] = React.useState<SortEntry[]>([])\n  const toggle = React.useCallback((key: string) => {\n    setSorts((prev) => {\n      const found = prev.find((s) => s.key === key)\n      const rest = prev.filter((s) => s.key !== key)\n      if (!found) return [...rest, { key, dir: \"asc\" as Dir }]\n      if (found.dir === \"asc\") return [...rest, { key, dir: \"desc\" as Dir }]\n      return rest\n    })\n  }, [])\n  const dirOf = React.useCallback(\n    (key: string): Dir | null => sorts.find((s) => s.key === key)?.dir ?? null,\n    [sorts]\n  )\n  const rankOf = React.useCallback(\n    (key: string): number => sorts.findIndex((s) => s.key === key) + 1,\n    [sorts]\n  )\n  return { sorts, toggle, dirOf, rankOf }\n}\n\nfunction ariaSort(dir: Dir | null): \"ascending\" | \"descending\" | \"none\" {\n  return dir === \"asc\" ? \"ascending\" : dir === \"desc\" ? \"descending\" : \"none\"\n}\n\n/* Sort: sade baslik butonlari, aktif kolon vurgulanir. */\nexport function SortTable({ className, size = \"md\", columns, rows }: TableProps) {\n  const { cols, data } = resolve(columns, rows)\n  const { sorts, toggle, dirOf } = useSingleSort()\n  const sorted = React.useMemo(() => sortRows(data, sorts), [data, sorts])\n\n  return (\n    <div\n      data-slot=\"styled-table\"\n      className={cn(\"w-full overflow-x-auto rounded-lg border border-border\", className)}\n    >\n      <table className={tableBase}>\n        <thead>\n          <tr className=\"border-b border-border bg-muted\">\n            {cols.map((col) => {\n              const dir = dirOf(col.key)\n              return (\n                <th\n                  key={col.key}\n                  scope=\"col\"\n                  aria-sort={ariaSort(dir)}\n                  className={cn(headBase, headCell[size], dir && \"text-foreground\")}\n                >\n                  <button type=\"button\" className={sortBtn} onClick={() => toggle(col.key)}>\n                    {col.label}\n                    {dir === \"asc\" ? <ArrowUp /> : dir === \"desc\" ? <ArrowDown /> : null}\n                  </button>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((row, i) => (\n            <tr key={i} className=\"border-b border-border/60 last:border-b-0\">\n              {cols.map((col) => (\n                <td key={col.key} className={cell[size]}>\n                  {row[col.key]}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\n/* Arrows: her baslikta cift ok, aktif yon dolu okla belirtilir. */\nexport function ArrowsTable({ className, size = \"md\", columns, rows }: TableProps) {\n  const { cols, data } = resolve(columns, rows)\n  const { sorts, toggle, dirOf } = useSingleSort()\n  const sorted = React.useMemo(() => sortRows(data, sorts), [data, sorts])\n\n  return (\n    <div\n      data-slot=\"styled-table\"\n      className={cn(\"w-full overflow-x-auto rounded-lg border border-border\", className)}\n    >\n      <table className={tableBase}>\n        <thead>\n          <tr className=\"border-b border-border\">\n            {cols.map((col) => {\n              const dir = dirOf(col.key)\n              return (\n                <th\n                  key={col.key}\n                  scope=\"col\"\n                  aria-sort={ariaSort(dir)}\n                  className={cn(headBase, headCell[size], dir && \"text-foreground\")}\n                >\n                  <button type=\"button\" className={sortBtn} onClick={() => toggle(col.key)}>\n                    {col.label}\n                    <span className=\"ml-auto inline-flex flex-col leading-none\">\n                      <ChevronUp\n                        className={cn(\"-mb-1\", dir === \"asc\" ? \"text-primary\" : \"text-muted-foreground/50\")}\n                      />\n                      <ChevronDown\n                        className={cn(dir === \"desc\" ? \"text-primary\" : \"text-muted-foreground/50\")}\n                      />\n                    </span>\n                  </button>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((row, i) => (\n            <tr key={i} className=\"border-b border-border/60 last:border-b-0\">\n              {cols.map((col) => (\n                <td key={col.key} className={cell[size]}>\n                  {row[col.key]}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\n/* Indicator: aktif kolon primary tinti + ustte gosterge cizgisi alir. */\nexport function IndicatorTable({ className, size = \"md\", columns, rows }: TableProps) {\n  const { cols, data } = resolve(columns, rows)\n  const { sorts, toggle, dirOf } = useSingleSort()\n  const sorted = React.useMemo(() => sortRows(data, sorts), [data, sorts])\n  const activeKey = sorts[0]?.key ?? null\n\n  return (\n    <div\n      data-slot=\"styled-table\"\n      className={cn(\"w-full overflow-x-auto rounded-lg border border-border\", className)}\n    >\n      <table className={tableBase}>\n        <thead>\n          <tr className=\"border-b border-border bg-muted\">\n            {cols.map((col) => {\n              const dir = dirOf(col.key)\n              return (\n                <th\n                  key={col.key}\n                  scope=\"col\"\n                  aria-sort={ariaSort(dir)}\n                  className={cn(\n                    headBase,\n                    headCell[size],\n                    \"relative\",\n                    dir &&\n                      \"text-primary before:absolute before:inset-x-0 before:top-0 before:h-0.5 before:bg-primary\"\n                  )}\n                >\n                  <button type=\"button\" className={sortBtn} onClick={() => toggle(col.key)}>\n                    {col.label}\n                    {dir === \"asc\" ? <ArrowUp /> : dir === \"desc\" ? <ArrowDown /> : <ArrowDownUp className=\"opacity-40\" />}\n                  </button>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((row, i) => (\n            <tr key={i} className=\"border-b border-border/60 last:border-b-0\">\n              {cols.map((col) => (\n                <td\n                  key={col.key}\n                  className={cn(\n                    cell[size],\n                    col.key === activeKey &&\n                      \"bg-[color-mix(in_oklab,var(--color-primary)_8%,transparent)] font-medium\"\n                  )}\n                >\n                  {row[col.key]}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\n/* Multi: birden fazla kolonda siralama, rozet ile oncelik sirasi gosterilir. */\nexport function MultiTable({ className, size = \"md\", columns, rows }: TableProps) {\n  const { cols, data } = resolve(columns, rows)\n  const { sorts, toggle, dirOf, rankOf } = useMultiSort()\n  const sorted = React.useMemo(() => sortRows(data, sorts), [data, sorts])\n\n  return (\n    <div\n      data-slot=\"styled-table\"\n      className={cn(\"w-full overflow-x-auto rounded-lg border border-border\", className)}\n    >\n      <table className={tableBase}>\n        <thead>\n          <tr className=\"border-b border-border bg-muted\">\n            {cols.map((col) => {\n              const dir = dirOf(col.key)\n              const rank = rankOf(col.key)\n              return (\n                <th\n                  key={col.key}\n                  scope=\"col\"\n                  aria-sort={ariaSort(dir)}\n                  className={cn(headBase, headCell[size], dir && \"text-foreground\")}\n                >\n                  <button type=\"button\" className={sortBtn} onClick={() => toggle(col.key)}>\n                    {col.label}\n                    {dir === \"asc\" ? <ArrowUp /> : dir === \"desc\" ? <ArrowDown /> : null}\n                    {rank > 0 ? (\n                      <span className=\"ml-auto inline-flex size-4 items-center justify-center rounded-full bg-primary text-[10px] leading-none font-semibold text-primary-foreground\">\n                        {rank}\n                      </span>\n                    ) : null}\n                  </button>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((row, i) => (\n            <tr key={i} className=\"border-b border-border/60 last:border-b-0\">\n              {cols.map((col) => (\n                <td key={col.key} className={cell[size]}>\n                  {row[col.key]}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\n/* Sticky: kaydirilan govdenin ustunde yapiskan, siralanabilir baslik. */\nexport function StickyTable({ className, size = \"md\", columns, rows }: TableProps) {\n  const cols = columns && columns.length > 0 ? columns : defaultColumns\n  const { sorts, toggle, dirOf } = useSingleSort()\n  /* A longer list by default so the sticky header is actually visible. */\n  const base = React.useMemo(\n    () =>\n      rows && rows.length > 0\n        ? rows\n        : defaultRows.concat(\n            defaultRows.map((r) => ({ ...r, name: `${String(r.name)} II` })),\n            defaultRows.map((r) => ({ ...r, name: `${String(r.name)} III` }))\n          ),\n    [rows]\n  )\n  const sorted = React.useMemo(() => sortRows(base, sorts), [base, sorts])\n\n  return (\n    <div\n      data-slot=\"styled-table\"\n      className={cn(\n        \"max-h-64 w-full overflow-auto rounded-lg border border-border\",\n        className\n      )}\n    >\n      <table className={tableBase}>\n        <thead className=\"sticky top-0 z-10\">\n          <tr className=\"bg-muted\">\n            {cols.map((col) => {\n              const dir = dirOf(col.key)\n              return (\n                <th\n                  key={col.key}\n                  scope=\"col\"\n                  aria-sort={ariaSort(dir)}\n                  className={cn(\n                    headBase,\n                    headCell[size],\n                    \"border-b border-border bg-muted\",\n                    dir && \"text-foreground\"\n                  )}\n                >\n                  <button type=\"button\" className={sortBtn} onClick={() => toggle(col.key)}>\n                    {col.label}\n                    {dir === \"asc\" ? <ArrowUp /> : dir === \"desc\" ? <ArrowDown /> : null}\n                  </button>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((row, i) => (\n            <tr key={i} className=\"border-b border-border/60 last:border-b-0\">\n              {cols.map((col) => (\n                <td key={col.key} className={cell[size]}>\n                  {row[col.key]}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/table-sortable.tsx"
    }
  ],
  "categories": [
    "styled",
    "table"
  ],
  "type": "registry:component"
}