{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "DataTable",
  "description": "جدول داده با مرتب‌سازی (aria-sort)، انتخاب ردیف‌ها و حالت خالی",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/DataTable.tsx",
      "type": "registry:ui",
      "target": "components/kashi/DataTable.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport interface Column<T> {\n  key: string;\n  header: ReactNode;\n  /** محتوای خانه؛ پیش‌فرض row[key] */\n  render?: (row: T) => ReactNode;\n  /** مقدار برای مرتب‌سازی؛ پیش‌فرض row[key] */\n  sortValue?: (row: T) => string | number;\n  sortable?: boolean;\n  /** عدد و مبلغ: چپ‌چین در RTL با ارقام هم‌عرض */\n  numeric?: boolean;\n  /** چینش خانه‌ها؛ numeric خودکار «end» است */\n  align?: \"start\" | \"center\" | \"end\";\n  width?: string;\n  /** پنهان کردن ستون در صفحه‌های باریک */\n  hideBelow?: \"sm\" | \"md\";\n  /** کلاس خانه‌های این ستون */\n  className?: string;\n}\n\nexport type SortState = { key: string; direction: \"ascending\" | \"descending\" } | null;\n\nexport interface DataTableProps<T> {\n  columns: Column<T>[];\n  rows: T[];\n  getRowId: (row: T) => string;\n  /** عنوان جدول (caption)؛ برای صفحه‌خوان ضروری است */\n  caption: ReactNode;\n  description?: ReactNode;\n  /** پنهان کردن عنوان از دید (صفحه‌خوان همچنان می‌خواند) */\n  hideCaption?: boolean;\n  selectable?: boolean;\n  selected?: string[];\n  onSelectedChange?: (ids: string[]) => void;\n  sort?: SortState;\n  defaultSort?: SortState;\n  onSortChange?: (sort: SortState) => void;\n  /** وقتی ردیفی نیست */\n  empty?: ReactNode;\n  compact?: boolean;\n  /** ردیف‌های راه‌راه */\n  striped?: boolean;\n  /** سرستون هنگام اسکرول ثابت بماند؛ با maxHeight */\n  stickyHeader?: boolean;\n  /** بیشترین ارتفاع جدول (مثل \"24rem\")؛ بیشتر از آن اسکرول می‌خورد */\n  maxHeight?: string;\n  /** نمایش ردیف‌های اسکلت به‌جای داده */\n  loading?: boolean | number;\n  /** کلیک روی ردیف؛ برای ناوبری بهتر است در یک خانه لینک واقعی هم بگذارید */\n  onRowClick?: (row: T) => void;\n  /** محتوای پایین جدول، مثل صفحه‌بندی */\n  footer?: ReactNode;\n  labels?: { selectAll?: string; selectRow?: (row: T) => string; loading?: string };\n  classNames?: { root?: string; table?: string; row?: string; cell?: string; header?: string; footer?: string };\n  className?: string;\n}\n\n/** جدول داده با مرتب‌سازی (aria-sort)، انتخاب ردیف‌ها و حالت خالی */\nexport function DataTable<T extends Record<string, unknown>>({\n  columns, rows, getRowId, caption, description, hideCaption, selectable, selected, onSelectedChange,\n  sort, defaultSort = null, onSortChange, empty, compact, striped, stickyHeader, maxHeight, loading, onRowClick, footer,\n  labels = {}, classNames = {}, className,\n}: DataTableProps<T>) {\n  const id = useId();\n  const [innerSort, setInnerSort] = useState<SortState>(defaultSort);\n  const [innerSel, setInnerSel] = useState<string[]>([]);\n  const activeSort = sort !== undefined ? sort : innerSort;\n  const sel = selected ?? innerSel;\n  const allRef = useRef<HTMLInputElement>(null);\n\n  const sorted = useMemo(() => {\n    if (!activeSort) return rows;\n    const col = columns.find((c) => c.key === activeSort.key);\n    const val = (r: T) => (col?.sortValue ? col.sortValue(r) : (r[activeSort.key] as string | number));\n    const dir = activeSort.direction === \"ascending\" ? 1 : -1;\n    return [...rows].sort((a, b) => {\n      const x = val(a);\n      const y = val(b);\n      return (typeof x === \"number\" && typeof y === \"number\" ? x - y : String(x).localeCompare(String(y), \"fa\")) * dir;\n    });\n  }, [rows, activeSort, columns]);\n\n  const setSort = (key: string) => {\n    const next: SortState =\n      activeSort?.key === key && activeSort.direction === \"ascending\" ? { key, direction: \"descending\" } : { key, direction: \"ascending\" };\n    if (sort === undefined) setInnerSort(next);\n    onSortChange?.(next);\n  };\n  const setSel = (ids: string[]) => {\n    if (selected === undefined) setInnerSel(ids);\n    onSelectedChange?.(ids);\n  };\n\n  const ids = rows.map(getRowId);\n  const all = ids.length > 0 && ids.every((x) => sel.includes(x));\n  const some = !all && ids.some((x) => sel.includes(x));\n  useEffect(() => {\n    if (allRef.current) allRef.current.indeterminate = some;\n  }, [some]);\n\n  const { messages: M } = useKashi();\n  const { selectAll = M.table.selectAll, selectRow = () => M.table.selectRow, loading: loadingLabel = M.common.loading } = labels;\n  const loadingRows = loading === true ? 5 : typeof loading === \"number\" ? loading : 0;\n  const cellCls = (c: Column<T>) =>\n    cx(c.numeric && \"k-table__num\", c.align && !c.numeric && `k-table__align-${c.align}`, c.hideBelow && `k-table__hide-${c.hideBelow}`, c.className, classNames.cell) || undefined;\n  const colCount = columns.length + (selectable ? 1 : 0);\n\n  return (\n    <div\n      className={cx(\"k-table-wrap\", stickyHeader && \"k-table-wrap--sticky\", className, classNames.root)}\n      role=\"region\"\n      aria-labelledby={`${id}-caption`}\n      aria-busy={loadingRows > 0 || undefined}\n      tabIndex={0}\n      style={maxHeight ? { maxBlockSize: maxHeight } : undefined}\n    >\n      <table className={cx(\"k-table\", compact && \"k-table--compact\", striped && \"k-table--striped\", onRowClick && \"k-table--clickable\", classNames.table)}>\n        <caption id={`${id}-caption`} className={hideCaption ? \"k-sr-only\" : undefined}>\n          <span className=\"k-table__caption-title\">{caption}</span>\n          {description && <span className=\"k-table__caption-desc\">{description}</span>}\n        </caption>\n        <thead>\n          <tr>\n            {selectable && (\n              <th scope=\"col\" className=\"k-table__check\">\n                <input ref={allRef} type=\"checkbox\" className=\"k-checkbox\" aria-label={selectAll} checked={all} onChange={() => setSel(all ? [] : ids)} />\n              </th>\n            )}\n            {columns.map((c) => {\n              const s = activeSort?.key === c.key ? activeSort.direction : undefined;\n              return (\n                <th key={c.key} scope=\"col\" aria-sort={c.sortable ? (s ?? \"none\") : undefined} className={cx(cellCls(c), classNames.header) || undefined} style={c.width ? { inlineSize: c.width } : undefined}>\n                  {c.sortable ? <button type=\"button\" className=\"k-table__sort\" onClick={() => setSort(c.key)}>{c.header}</button> : c.header}\n                </th>\n              );\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {loadingRows > 0 ? (\n            Array.from({ length: loadingRows }, (_, i) => (\n              <tr key={`sk-${i}`} aria-hidden=\"true\">\n                {selectable && <td className=\"k-table__check\"><span className=\"k-skeleton\" style={{ inlineSize: \"1.125rem\", blockSize: \"1.125rem\" }} /></td>}\n                {columns.map((c) => (\n                  <td key={c.key} className={cellCls(c)}><span className=\"k-skeleton\" style={{ inlineSize: `${50 + ((i * 17 + c.key.length * 11) % 40)}%` }} /></td>\n                ))}\n              </tr>\n            ))\n          ) : sorted.length === 0 ? (\n            <tr className=\"k-table__empty\"><td colSpan={colCount}>{empty ?? <p style={{ padding: \"2rem\", margin: 0, textAlign: \"center\" }}>{M.table.empty}</p>}</td></tr>\n          ) : (\n            sorted.map((r) => {\n              const rid = getRowId(r);\n              const on = sel.includes(rid);\n              return (\n                <tr\n                  key={rid}\n                  aria-selected={selectable ? on : undefined}\n                  className={classNames.row}\n                  onClick={onRowClick ? (e) => {\n                    // کلیک روی چک‌باکس، دکمه یا لینک داخل ردیف را ردیف نگیرد\n                    if ((e.target as HTMLElement).closest(\"a, button, input, label, select, textarea\")) return;\n                    onRowClick(r);\n                  } : undefined}\n                >\n                  {selectable && (\n                    <td className=\"k-table__check\">\n                      <input type=\"checkbox\" className=\"k-checkbox\" aria-label={selectRow(r)} checked={on} onChange={() => setSel(on ? sel.filter((x) => x !== rid) : [...sel, rid])} />\n                    </td>\n                  )}\n                  {columns.map((c) => (\n                    <td key={c.key} className={cellCls(c)}>{c.render ? c.render(r) : (r[c.key] as ReactNode)}</td>\n                  ))}\n                </tr>\n              );\n            })\n          )}\n        </tbody>\n      </table>\n      {loadingRows > 0 && <span className=\"k-sr-only\" role=\"status\">{loadingLabel}</span>}\n      {footer && <div className={cx(\"k-table__footer\", classNames.footer)}>{footer}</div>}\n    </div>\n  );\n}\n"
    }
  ]
}