{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Tabela densa: ordenação, seleção, edição inline e menu de contexto, em qualquer app React. Sessão e URL ficam com você: canEdit libera a edição e onSortingChange avisa a ordenação. Quem já instalou precisa passar essas props.",
  "dependencies": [
    "@tanstack/react-table@^8.21.3",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "checkbox",
    "context-menu",
    "input",
    "skeleton",
    "table",
    "@visant/inline-edit",
    "@visant/use-media-query"
  ],
  "files": [
    {
      "path": "registry/visant/ui/data-table.tsx",
      "content": "'use client';\n\n/*\n * Esta tabela é escrita contra a API v8 do TanStack Table (`getIsSelected`,\n * `toggleAllPageRowsSelected`, `ColumnDef` com genérico de duas casas). A v9\n * reorganizou tudo em features compostas e nada disso existe mais lá.\n *\n * Por isso a dependência do item vai PINADA em `^8.21.3` no `registry.json`.\n * Sem o pino o `shadcn add` instalava a última, e o projeto do cliente quebrava\n * no `tsc` com treze erros de tipo num arquivo que ele nunca abriu — foi assim\n * que o teste de instalação limpa acusou. Migrar pra v9 é trabalho de verdade;\n * o pino é o que impede o estrago enquanto isso não acontece.\n *\n * ── Sessão e URL são de quem usa ───────────────────────────────────────────\n *\n * A tabela não conhece framework nem autenticação. Quem instalou uma versão\n * anterior, que lia a sessão e escrevia `?sort=` sozinha, precisa passar as props.\n * Sem elas a edição fica desligada e a ordenação vive só no estado da tabela.\n *\n * A receita em Next, com `useSession`, `useRouter` e `useSearchParams`:\n *\n *   const [id, dir] = (params.get('sort') ?? '').split('.');\n *   <DataTable\n *     canEdit={session?.user?.role === 'admin'}\n *     initialSorting={id ? [{ id, desc: dir === 'desc' }] : undefined}\n *     onSortingChange={([s]) => {\n *       const q = new URLSearchParams(params.toString());\n *       if (s) q.set('sort', `${s.id}.${s.desc ? 'desc' : 'asc'}`);\n *       else q.delete('sort');\n *       router.replace(`?${q}`, { scroll: false });\n *     }}\n *   />\n */\n\nimport * as React from 'react';\n\nimport {\n  type ColumnDef,\n  type VisibilityState,\n  type ColumnPinningState,\n  type RowSelectionState,\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  type SortingState,\n  useReactTable,\n} from '@tanstack/react-table';\nimport { InlineEdit } from './inline-edit';\nimport { Input } from '@/components/ui/input';\nimport {\n  TableBody,\n  TableCell,\n  TableFooter,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table';\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuTrigger,\n  ContextMenuSeparator,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuCheckboxItem,\n  ContextMenuShortcut,\n  ContextMenuLabel,\n} from '@/components/ui/context-menu';\nimport { ArrowUpDown, ArrowUp, ArrowDown, Trash, Columns, EyeOff, Copy, Search, Pin, PinOff, ArrowLeft, ArrowRight } from 'lucide-react';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { cn } from '@/lib/utils';\nimport { useMediaQuery } from '@/hooks/use-media-query';\nimport { Button } from '@/components/ui/button';\nimport {\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight\n} from 'lucide-react';\n\nexport interface DataTableProps<TData, TValue> {\n  columns: ColumnDef<TData, TValue>[];\n  data: TData[];\n  isLoading?: boolean;\n  emptyMessage?: string;\n  onRowClick?: (row: TData) => void;\n  className?: string;\n  /** Compact style (px-3 py-2) for admin tables. Default true. */\n  compact?: boolean;\n  /** Optional footer (e.g. <TableRow>...</TableRow>) */\n  footer?: React.ReactNode;\n  /** Optional callback for deleting a row via context menu */\n  onDeleteRow?: (row: TData) => void;\n  onDuplicateRow?: (row: TData) => void;\n  /** Optional extra context-menu items rendered at the top of the row menu. */\n  renderRowContextMenu?: (row: TData) => React.ReactNode;\n  /** Enable a leading checkbox column with shift-click range selection. */\n  enableRowSelection?: boolean;\n  /** Stable row id (e.g. the issue id) so selection survives re-sorts/refetches. */\n  getRowId?: (row: TData) => string;\n  /** Rendered above the table when ≥1 row is selected. */\n  renderBatchBar?: (selected: TData[], clear: () => void) => React.ReactNode;\n  /**\n   * The ID of the column that should be treated as \"primary\".\n   * This column will have width: auto (hug content/fill) and take remaining space.\n   */\n  primaryColumnId?: string;\n  /** Optional callback for updating a cell value inline (only for admins) */\n  onCellUpdate?: (row: TData, columnId: string, value: any) => Promise<void>;\n  initialColumnVisibility?: VisibilityState;\n  /**\n   * Initial column pinning (e.g. `{ right: ['actions'] }` to freeze an actions\n   * column to the right edge). Opt-in — admin tables are unaffected when omitted.\n   */\n  initialColumnPinning?: ColumnPinningState;\n  /**\n   * Desliga a ordenação por cabeçalho. Use quando a tela já tem um controle de\n   * ordenação próprio — dois eixos de ordenação sobre a mesma tabela se\n   * contradizem (o controle externo continua dizendo \"Prazo\" enquanto a tabela\n   * já reordenou por outra coluna).\n   */\n  disableSorting?: boolean;\n  /** Ordenação com que a tabela nasce. */\n  initialSorting?: SortingState;\n  /** Avisa cada mudança de ordenação, para gravar na URL ou no servidor. */\n  onSortingChange?: (sorting: SortingState) => void;\n  /**\n   * Libera a edição inline das colunas com `meta.editable` (precisa de\n   * `onCellUpdate`). Desligada por padrão: a permissão é decisão de quem usa.\n   */\n  canEdit?: boolean;\n  /**\n   * Title for the table toolbar (optional)\n   */\n  title?: string;\n  /**\n   * Optional initial page size. Default 20.\n   */\n  pageSize?: number;\n  /**\n   * Hide pagination controls. Default false.\n   */\n  hidePagination?: boolean;\n  /**\n   * Hide the built-in toolbar search box. Use when the parent already provides\n   * its own search input feeding the data (avoids two competing search fields).\n   * Default false.\n   */\n  hideToolbarSearch?: boolean;\n  /**\n   * Render the batch action bar in normal flow (full-width, wraps and pushes the\n   * table down) instead of the default absolute overlay. Only safe when the\n   * toolbar search is hidden; otherwise the bar collides with the search box.\n   * Default false (absolute overlay).\n   */\n  batchBarInFlow?: boolean;\n}\n\nexport function DataTable<TData, TValue>({\n  columns,\n  data,\n  isLoading = false,\n  emptyMessage = 'Nenhum resultado.',\n  onRowClick,\n  className,\n  compact = true,\n  footer,\n  onDeleteRow,\n  onDuplicateRow,\n  renderRowContextMenu,\n  enableRowSelection = false,\n  getRowId,\n  renderBatchBar,\n  primaryColumnId,\n  onCellUpdate,\n  initialColumnVisibility = {},\n  initialColumnPinning,\n  disableSorting = false,\n  initialSorting = [],\n  onSortingChange,\n  canEdit = false,\n  title,\n  pageSize = 20,\n  hidePagination = false,\n  hideToolbarSearch = false,\n  batchBarInFlow = false,\n}: DataTableProps<TData, TValue>) {\n  const [sorting, setSorting] = React.useState<SortingState>(initialSorting);\n  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialColumnVisibility);\n  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>(initialColumnPinning ?? {});\n\n  // Simplified Width State: Default to \"Fit\" (true) on Desktop, always false on Mobile\n  const isMobile = useMediaQuery('(max-width: 768px)');\n\n  // Só avisa quando muda: o estado com que a tabela nasceu já é de quem passou.\n  const ordenacaoAvisada = React.useRef(sorting);\n  React.useEffect(() => {\n    if (ordenacaoAvisada.current === sorting) return;\n    ordenacaoAvisada.current = sorting;\n    onSortingChange?.(sorting);\n  }, [sorting, onSortingChange]);\n\n  const [globalFilter, setGlobalFilter] = React.useState('');\n  const [editingCell, setEditingCell] = React.useState<{ rowId: string; colId: string } | null>(null);\n  const [tempValue, setTempValue] = React.useState<any>(null);\n  const [undoToast, setUndoToast] = React.useState<string | null>(null);\n\n  // Undo stack: stores previous values before onCellUpdate commits them\n  type UndoEntry = { row: TData; columnId: string; oldValue: any; label: string };\n  const undoStack = React.useRef<UndoEntry[]>([]);\n  const previousValue = React.useRef<any>(null);\n\n  const pushUndo = React.useCallback((entry: UndoEntry) => {\n    undoStack.current = [...undoStack.current.slice(-19), entry]; // keep last 20\n  }, []);\n\n  const handleUndo = React.useCallback(async () => {\n    if (!onCellUpdate || undoStack.current.length === 0) return;\n    const entry = undoStack.current[undoStack.current.length - 1]!;\n    undoStack.current = undoStack.current.slice(0, -1);\n    await onCellUpdate(entry.row, entry.columnId, entry.oldValue);\n    setUndoToast(`Desfeito: ${entry.label}`);\n    setTimeout(() => setUndoToast(null), 2200);\n  }, [onCellUpdate]);\n\n  // Shift+Scroll → horizontal scroll; Ctrl+Z → undo\n  const scrollContainerRef = React.useRef<HTMLDivElement>(null);\n  React.useEffect(() => {\n    const el = scrollContainerRef.current;\n    if (!el) return;\n    const onWheel = (e: WheelEvent) => {\n      if (e.shiftKey && e.deltaY !== 0) {\n        e.preventDefault();\n        el.scrollLeft += e.deltaY;\n      }\n    };\n    el.addEventListener('wheel', onWheel, { passive: false });\n    return () => el.removeEventListener('wheel', onWheel);\n  }, []);\n\n  React.useEffect(() => {\n    if (!onCellUpdate) return;\n    const onKeyDown = (e: KeyboardEvent) => {\n      if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {\n        // Digitando numa célula, Ctrl+Z é do TEXTO. Sem esta guarda a tabela\n        // roubava o undo nativo do campo e revertia a célula anterior inteira\n        // no lugar de apagar a última letra. (O GlobalUndoListener já tinha a\n        // guarda; esta pilha local não.)\n        const focado = document.activeElement;\n        if (\n          focado?.tagName === 'INPUT' ||\n          focado?.tagName === 'TEXTAREA' ||\n          (focado as HTMLElement | null)?.isContentEditable\n        ) {\n          return;\n        }\n        // Só consome o atalho se HÁ o que desfazer aqui. Com a pilha vazia o\n        // preventDefault incondicional roubava o Ctrl+Z do GlobalUndoListener\n        // (que escuta em `window`, depois deste `document` no bubbling) e o\n        // usuário perdia o undo da seção. Undo mais local vence, mas só quando\n        // tem o que desfazer.\n        if (undoStack.current.length === 0) return;\n        e.preventDefault();\n        handleUndo();\n      }\n    };\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, [onCellUpdate, handleUndo]);\n\n  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});\n  const lastSelectedIndex = React.useRef<number | null>(null);\n\n  // Shift-click selects the range between the last toggled row and this one.\n  const handleSelectClick = React.useCallback(\n    (e: React.MouseEvent, row: any, rows: any[]) => {\n      const idx = rows.findIndex((r) => r.id === row.id);\n      const want = !row.getIsSelected();\n      if (e.shiftKey && lastSelectedIndex.current !== null && idx !== -1) {\n        const a = Math.min(lastSelectedIndex.current, idx);\n        const b = Math.max(lastSelectedIndex.current, idx);\n        for (let i = a; i <= b; i++) rows[i]?.toggleSelected(want);\n      } else {\n        row.toggleSelected(want);\n      }\n      lastSelectedIndex.current = idx;\n    },\n    []\n  );\n\n  const tableColumns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {\n    if (!enableRowSelection) return columns;\n    const selectCol: ColumnDef<TData, TValue> = {\n      id: '__select',\n      enableSorting: false,\n      enableHiding: false,\n      size: 36,\n      header: ({ table }) => (\n        <div className=\"px-1\" onClick={(e) => e.stopPropagation()}>\n          <Checkbox\n            checked={\n              table.getIsAllPageRowsSelected() ||\n              (table.getIsSomePageRowsSelected() ? 'indeterminate' : false)\n            }\n            onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}\n            aria-label=\"Selecionar todos\"\n          />\n        </div>\n      ),\n      cell: ({ row, table }) => (\n        <div\n          className=\"px-1\"\n          onClick={(e) => {\n            e.stopPropagation();\n            handleSelectClick(e, row, table.getRowModel().rows);\n          }}\n        >\n          <Checkbox\n            checked={row.getIsSelected()}\n            aria-label=\"Selecionar linha\"\n            className=\"pointer-events-none\"\n          />\n        </div>\n      ),\n    };\n    return [selectCol, ...columns];\n  }, [columns, enableRowSelection, handleSelectClick]);\n\n  // Mobile: colunas marcadas com `meta.hideOnMobile` somem — menos scroll\n  // horizontal por linha. Não toca no estado base: voltando ao desktop, a\n  // visibilidade escolhida pelo usuário reaparece intacta.\n  // <640 (sm), NÃO o isMobile de 768 acima: 640–768 é \"desktop\" pro resto\n  // do app e o comportamento ali não pode mudar.\n  const isNarrow = useMediaQuery('(max-width: 639px)');\n  const effectiveColumnVisibility = React.useMemo<VisibilityState>(() => {\n    if (!isNarrow) return columnVisibility;\n    const forced: VisibilityState = {};\n    for (const col of tableColumns) {\n      if ((col.meta as any)?.hideOnMobile) {\n        const id = col.id ?? (col as any).accessorKey;\n        if (id) forced[id] = false;\n      }\n    }\n    return { ...columnVisibility, ...forced };\n  }, [isNarrow, columnVisibility, tableColumns]);\n\n  const table = useReactTable({\n    data,\n    columns: tableColumns,\n    ...(getRowId ? { getRowId } : {}),\n    enableRowSelection,\n    enableSorting: !disableSorting,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    enableColumnResizing: false,\n    state: {\n      sorting,\n      columnVisibility: effectiveColumnVisibility,\n      globalFilter,\n      columnPinning,\n      rowSelection,\n    },\n    onRowSelectionChange: setRowSelection,\n    onSortingChange: setSorting,\n    onColumnVisibilityChange: setColumnVisibility,\n    onGlobalFilterChange: setGlobalFilter,\n    onColumnPinningChange: setColumnPinning,\n    initialState: {\n      pagination: {\n        pageSize: pageSize,\n      },\n    },\n  });\n\n  const cellClass = compact ? 'px-3 py-2' : 'p-4';\n  const headClass = compact ? 'px-3 py-2' : 'p-4';\n\n  // Loading não apaga a moldura: o <thead> tem os headers de coluna (estáticos,\n  // vêm de `columns`) e a toolbar/título são conhecidos antes do dado. Só o\n  // corpo vira skeleton rows — a tabela já \"parece\" a final, sem spinner solto.\n  const skeletonRowCount = Math.min(pageSize, 8);\n  // Larguras variadas por coluna (determinístico) pra não virar um grid de\n  // barras idênticas — lê como dado tabular de verdade.\n  const SKELETON_WIDTHS = ['70%', '45%', '85%', '55%', '35%', '65%'];\n\n  return (\n    <div className={cn('space-y-2 relative', className)}>\n      {/* Undo toast */}\n      {undoToast && (\n        <div className=\"fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-xs shadow-lg animate-in fade-in slide-in-from-bottom-2 duration-200\">\n          <span className=\"text-muted-foreground\">{undoToast}</span>\n          {undoStack.current.length > 0 && (\n            <button\n              className=\"text-foreground font-medium hover:underline underline-offset-2\"\n              onClick={handleUndo}\n            >\n              Desfazer mais\n            </button>\n          )}\n        </div>\n      )}\n      {/* Table Toolbar - Hidden on Mobile if empty, or just hides the toggle */}\n      {(title || !hideToolbarSearch || (enableRowSelection && renderBatchBar)) && (\n      <div className=\"relative flex items-center justify-between gap-2\">\n        {title && <h3 className=\"font-semibold text-sm\">{title}</h3>}\n\n        {/* Built-in search — skip when the parent owns search (hideToolbarSearch). */}\n        {!hideToolbarSearch && (\n          <div className=\"ml-auto flex items-center gap-2\">\n            <div className=\"relative\">\n              <Search className=\"absolute left-2 top-2.5 h-3.5 w-3.5 text-muted-foreground\" />\n              <Input\n                placeholder=\"Pesquisar...\"\n                value={globalFilter ?? ''}\n                onChange={(event) => setGlobalFilter(event.target.value)}\n                className=\"h-8 pointer-coarse:h-11 w-[150px] lg:w-[200px] pl-8 text-xs bg-background/50\"\n              />\n            </div>\n          </div>\n        )}\n\n        {/* Batch action bar. Default: absolute overlay (zero flow space, never\n            shifts the toolbar — safe alongside a visible search box). Opt-in\n            `batchBarInFlow` renders it full-width in normal flow, which wraps\n            cleanly on mobile but must only be used when the search is hidden. */}\n        {enableRowSelection && renderBatchBar && table.getSelectedRowModel().rows.length > 0 && (\n          <div className={cn(batchBarInFlow ? \"w-full min-w-0 z-30\" : \"absolute inset-x-0 top-1/2 -translate-y-1/2 z-30\")}>\n            {renderBatchBar(\n              table.getSelectedRowModel().rows.map((r) => r.original),\n              () => table.resetRowSelection()\n            )}\n          </div>\n        )}\n      </div>\n      )}\n\n      <div\n        ref={scrollContainerRef}\n        className={cn(\n          'group relative w-full max-w-full min-w-0 overflow-x-auto overflow-y-auto max-h-[calc(100vh-10rem)] rounded-md border touch-pan-x touch-pan-y',\n          // Custom Scrollbar for better UX\n          'scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent'\n        )}\n      >\n        <table\n          className={cn(\n            \"w-full caption-bottom text-sm table-auto min-w-full w-max\"\n          )}\n        >\n          <TableHeader className=\"sticky top-0 z-20\">\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow\n                key={headerGroup.id}\n                className=\"bg-background hover:bg-background group/row\"\n              >\n                {headerGroup.headers.map((header) => {\n                  /*\n                    O invólucro do header só é <button> quando a coluna ORDENA.\n                    Era sempre botão, e isso punha um <button> em volta do\n                    header da coluna de seleção — cujo conteúdo é um <Checkbox>,\n                    que no Radix também é <button>. Botão dentro de botão é HTML\n                    inválido: o React derrubava um \"hydration error\" no console\n                    de toda tela com DataTable + seleção, e o navegador é livre\n                    para remontar a árvore como quiser.\n\n                    De quebra, a coluna de ações parava de se anunciar como\n                    botão para o leitor de tela — botão que não faz nada.\n                  */\n                  const canSort = header.column.getCanSort();\n                  const triggerClass = cn(\n                    'group relative flex w-full items-center gap-1 text-xs font-medium text-muted-foreground transition-colors',\n                    canSort && 'cursor-pointer hover:text-foreground pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-3.5'\n                  );\n                  const headerBody = (\n                    <>\n                      <span className=\"truncate\">\n                        {flexRender(header.column.columnDef.header, header.getContext())}\n                      </span>\n                      {canSort && (\n                        <span\n                          className={cn(\n                            'ml-1 inline-flex h-3.5 w-3.5 items-center justify-center text-xs transition-[color,opacity]',\n                            header.column.getIsSorted()\n                              ? 'text-foreground opacity-100'\n                              : 'text-muted-foreground/40 opacity-0 group-hover:opacity-100'\n                          )}\n                        >\n                          {header.column.getIsSorted() === 'asc' && <ArrowUp className=\"h-3 w-3\" />}\n                          {header.column.getIsSorted() === 'desc' && (\n                            <ArrowDown className=\"h-3 w-3\" />\n                          )}\n                          {!header.column.getIsSorted() && (\n                            <ArrowUpDown className=\"h-3 w-3 opacity-60\" />\n                          )}\n                        </span>\n                      )}\n                      {header.column.getIsPinned() && (\n                        <Pin className=\"h-3 w-3 ml-1 text-primary opacity-60\" fill=\"currentColor\" />\n                      )}\n                    </>\n                  );\n\n                  return (\n                  <TableHead\n                    key={header.id}\n                    className={cn(\n                      'relative transition-[background-color,box-shadow] duration-200',\n                      headClass,\n                      header.column.getIsPinned() && \"z-20 bg-background/95 backdrop-blur-sm\",\n                      header.column.getIsPinned() === 'left' && \"sticky left-0 shadow-[1px_0_0_0_hsl(var(--border))]\",\n                      header.column.getIsPinned() === 'right' && \"sticky right-0 shadow-[-1px_0_0_0_hsl(var(--border))]\"\n                    )}\n                    style={{\n                      left: header.column.getIsPinned() === 'left' ? header.column.getStart('left') : undefined,\n                      right: header.column.getIsPinned() === 'right' ? header.column.getAfter('right') : undefined,\n                      width: header.column.columnDef.size !== 150 ? header.column.getSize() : undefined,\n                      minWidth: header.column.columnDef.minSize\n                    }}\n                  >\n                    {header.isPlaceholder ? null : (\n                      <ContextMenu>\n                        <ContextMenuTrigger asChild>\n                          <div className=\"flex h-full items-center\">\n                            {canSort ? (\n                              <button\n                                type=\"button\"\n                                onClick={header.column.getToggleSortingHandler()}\n                                className={triggerClass}\n                              >\n                                {headerBody}\n                              </button>\n                            ) : (\n                              <div className={triggerClass}>{headerBody}</div>\n                            )}\n                          </div>\n                        </ContextMenuTrigger>\n                        <ContextMenuContent>\n                          <ContextMenuLabel>Opções da Coluna</ContextMenuLabel>\n                          <ContextMenuSeparator />\n\n                          <ContextMenuSub>\n                            <ContextMenuSubTrigger>\n                              <Pin className=\"mr-2 h-4 w-4\" />\n                              Fixar / Congelar\n                            </ContextMenuSubTrigger>\n                            <ContextMenuSubContent className=\"w-48\">\n                              <ContextMenuItem\n                                onClick={() => header.column.pin('left')}\n                                disabled={header.column.getIsPinned() === 'left'}\n                              >\n                                <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                                Fixar à Esquerda\n                              </ContextMenuItem>\n                              <ContextMenuItem\n                                onClick={() => header.column.pin('right')}\n                                disabled={header.column.getIsPinned() === 'right'}\n                              >\n                                <ArrowRight className=\"mr-2 h-4 w-4\" />\n                                Fixar à Direita\n                              </ContextMenuItem>\n                              {header.column.getIsPinned() && (\n                                <>\n                                  <ContextMenuSeparator />\n                                  <ContextMenuItem onClick={() => header.column.pin(false)}>\n                                    <PinOff className=\"mr-2 h-4 w-4\" />\n                                    Desafixar\n                                  </ContextMenuItem>\n                                </>\n                              )}\n                            </ContextMenuSubContent>\n                          </ContextMenuSub>\n\n                          <ContextMenuSeparator />\n                          {header.column.getCanSort() && (\n                            <>\n                              <ContextMenuItem onClick={() => header.column.toggleSorting(false)}>\n                                <ArrowUp className=\"mr-2 h-4 w-4\" />\n                                Ordem Crescente\n                              </ContextMenuItem>\n                              <ContextMenuItem onClick={() => header.column.toggleSorting(true)}>\n                                <ArrowDown className=\"mr-2 h-4 w-4\" />\n                                Ordem Decrescente\n                              </ContextMenuItem>\n                              <ContextMenuItem onClick={() => header.column.clearSorting()}>\n                                <ArrowUpDown className=\"mr-2 h-4 w-4\" />\n                                Limpar Ordenação\n                              </ContextMenuItem>\n                              <ContextMenuSeparator />\n                            </>\n                          )}\n                          {header.column.getCanHide() && (\n                            <ContextMenuItem onClick={() => header.column.toggleVisibility(false)}>\n                              <EyeOff className=\"mr-2 h-4 w-4\" />\n                              Esconder Coluna\n                            </ContextMenuItem>\n                          )}\n                        </ContextMenuContent>\n                      </ContextMenu>\n                    )}\n                  </TableHead>\n                  );\n                })}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {isLoading ? (\n              Array.from({ length: skeletonRowCount }).map((_, i) => (\n                <TableRow key={`sk-${i}`} className=\"hover:bg-transparent\">\n                  {table.getVisibleLeafColumns().map((col, ci) => (\n                    <TableCell key={col.id} className={cellClass}>\n                      <Skeleton\n                        className=\"h-4\"\n                        style={{ width: SKELETON_WIDTHS[(i + ci) % SKELETON_WIDTHS.length] }}\n                      />\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : !table.getRowModel().rows?.length ? (\n              <TableRow>\n                <TableCell\n                  colSpan={columns.length}\n                  className={cn('h-24 text-center text-muted-foreground', cellClass)}\n                >\n                  {emptyMessage}\n                </TableCell>\n              </TableRow>\n            ) : (\n              table.getRowModel().rows.map((row) => (\n                <ContextMenu key={row.id}>\n                  <ContextMenuTrigger asChild>\n                    <TableRow\n                      data-state={row.getIsSelected() && 'selected'}\n                      className={cn(\n                        onRowClick && 'cursor-pointer',\n                        'hover:bg-muted/30 transition-colors'\n                      )}\n                      onClick={onRowClick ? () => onRowClick(row.original) : undefined}\n                      onKeyDown={\n                        onRowClick\n                          ? (e) => {\n                            if (e.key !== 'Enter' && e.key !== ' ') return;\n                            e.preventDefault();\n                            onRowClick(row.original);\n                          }\n                          : undefined\n                      }\n                      tabIndex={onRowClick ? 0 : undefined}\n                      role={onRowClick ? 'button' : undefined}\n                    >\n                      {row.getVisibleCells().map((cell) => {\n                        const isEditable = canEdit && !!onCellUpdate && (cell.column.columnDef.meta as any)?.editable;\n                        const isEditing = editingCell?.rowId === row.id && editingCell.colId === cell.id;\n\n                        return (\n                          <TableCell\n                            key={cell.id}\n                            className={cn(\n                              cellClass,\n                              'overflow-hidden transition-[background-color,box-shadow] duration-200',\n                              cell.column.getIsPinned() && \"z-10 bg-background/95 backdrop-blur-sm\",\n                              cell.column.getIsPinned() === 'left' && \"sticky left-0 shadow-[1px_0_0_0_hsl(var(--border))]\",\n                              cell.column.getIsPinned() === 'right' && \"sticky right-0 shadow-[-1px_0_0_0_hsl(var(--border))]\",\n                              isEditable && \"cursor-text\"\n                            )}\n                            style={{\n                              left: cell.column.getIsPinned() === 'left' ? cell.column.getStart('left') : undefined,\n                              right: cell.column.getIsPinned() === 'right' ? cell.column.getAfter('right') : undefined,\n                            }}\n                            onClick={(e) => {\n                              if (isEditable) e.stopPropagation();\n                            }}\n                          >\n                            {isEditable ? (\n                              <InlineEdit\n                                value={flexRender(cell.column.columnDef.cell, cell.getContext())}\n                                isEditing={isEditing}\n                                onStartEdit={() => {\n                                  previousValue.current = cell.getValue();\n                                  setEditingCell({ rowId: row.id, colId: cell.id });\n                                  setTempValue(cell.getValue());\n                                }}\n                                onSave={async () => {\n                                  if (tempValue !== previousValue.current) {\n                                    pushUndo({\n                                      row: row.original,\n                                      columnId: cell.column.id,\n                                      oldValue: previousValue.current,\n                                      label: `${cell.column.id}: ${String(previousValue.current ?? '–')}`,\n                                    });\n                                  }\n                                  await onCellUpdate(row.original, cell.column.id, tempValue);\n                                  setEditingCell(null);\n                                }}\n                                onCancel={() => setEditingCell(null)}\n                              >\n                                <Input\n                                  value={tempValue ?? ''}\n                                  onChange={(e) => setTempValue(e.target.value)}\n                                  onBlur={async () => {\n                                    if (tempValue !== previousValue.current) {\n                                      pushUndo({\n                                        row: row.original,\n                                        columnId: cell.column.id,\n                                        oldValue: previousValue.current,\n                                        label: `${cell.column.id}: ${String(previousValue.current ?? '–')}`,\n                                      });\n                                      await onCellUpdate(row.original, cell.column.id, tempValue);\n                                    }\n                                    setEditingCell(null);\n                                  }}\n                                  className=\"h-7 text-xs\"\n                                  autoFocus\n                                />\n                              </InlineEdit>\n                            ) : (\n                              flexRender(cell.column.columnDef.cell, cell.getContext())\n                            )}\n                          </TableCell>\n                        );\n                      })}\n                    </TableRow>\n                  </ContextMenuTrigger>\n                  <ContextMenuContent className=\"w-64\">\n                    {renderRowContextMenu && (\n                      <>\n                        {renderRowContextMenu(row.original)}\n                        <ContextMenuSeparator />\n                      </>\n                    )}\n                    <ContextMenuItem\n                      onClick={() => {\n                        if (typeof navigator !== 'undefined') {\n                          navigator.clipboard.writeText(JSON.stringify(row.original, null, 2));\n                        }\n                      }}\n                    >\n                      <Copy className=\"mr-2 h-4 w-4\" />\n                      Copiar Dados (JSON)\n                    </ContextMenuItem>\n                    {onDuplicateRow && (\n                      <ContextMenuItem onClick={() => onDuplicateRow(row.original)}>\n                        <Copy className=\"mr-2 h-4 w-4\" />\n                        Duplicar linha\n                      </ContextMenuItem>\n                    )}\n                    <ContextMenuSeparator />\n                    {onDeleteRow && (\n                      <>\n                        <ContextMenuItem\n                          className=\"text-destructive focus:text-destructive focus:bg-destructive/10\"\n                          onClick={() => onDeleteRow(row.original)}\n                        >\n                          <Trash className=\"mr-2 h-4 w-4\" />\n                          Apagar linha\n                          <ContextMenuShortcut>DEL</ContextMenuShortcut>\n                        </ContextMenuItem>\n                        <ContextMenuSeparator />\n                      </>\n                    )}\n                    <ContextMenuSub>\n                      <ContextMenuSubTrigger>\n                        <Columns className=\"mr-2 h-4 w-4\" />\n                        Colunas\n                      </ContextMenuSubTrigger>\n                      <ContextMenuSubContent className=\"w-48\">\n                        {table\n                          .getAllColumns()\n                          .filter((column) => column.getCanHide())\n                          // No mobile, colunas forçadas por hideOnMobile saem do\n                          // menu: o toggle gravaria no estado base sem efeito\n                          // visível (checkbox que \"não funciona\").\n                          .filter((column) => !(isNarrow && (column.columnDef.meta as any)?.hideOnMobile))\n                          .map((column) => {\n                            return (\n                              <ContextMenuCheckboxItem\n                                key={column.id}\n                                className=\"capitalize\"\n                                checked={column.getIsVisible()}\n                                onCheckedChange={(value) =>\n                                  column.toggleVisibility(!!value)\n                                }\n                              >\n                                {typeof column.columnDef.header === 'string'\n                                  ? column.columnDef.header\n                                  : column.id}\n                              </ContextMenuCheckboxItem>\n                            );\n                          })}\n                      </ContextMenuSubContent>\n                    </ContextMenuSub>\n                  </ContextMenuContent>\n                </ContextMenu>\n              ))\n            )}\n          </TableBody>\n          {footer && <TableFooter>{footer}</TableFooter>}\n        </table>\n      </div>\n\n      {!hidePagination && table.getPageCount() > 1 && (\n        <div className=\"flex items-center justify-between px-2 py-1\">\n          <div className=\"flex-1 text-xs text-muted-foreground whitespace-nowrap\">\n            {table.getFilteredRowModel().rows.length} total • Pág. {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}\n          </div>\n          <div className=\"flex items-center space-x-2\">\n            <div className=\"flex items-center space-x-1\">\n              <Button\n                variant=\"outline\"\n                className=\"hidden h-7 w-7 p-0 lg:flex\"\n                onClick={() => table.setPageIndex(0)}\n                disabled={!table.getCanPreviousPage()}\n              >\n                <span className=\"sr-only\">Primeira página</span>\n                <ChevronsLeft className=\"h-4 w-4\" />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"h-7 w-7 p-0\"\n                onClick={() => table.previousPage()}\n                disabled={!table.getCanPreviousPage()}\n              >\n                <span className=\"sr-only\">Página anterior</span>\n                <ChevronLeft className=\"h-4 w-4\" />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"h-7 w-7 p-0\"\n                onClick={() => table.nextPage()}\n                disabled={!table.getCanNextPage()}\n              >\n                <span className=\"sr-only\">Próxima página</span>\n                <ChevronRight className=\"h-4 w-4\" />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"hidden h-7 w-7 p-0 lg:flex\"\n                onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n                disabled={!table.getCanNextPage()}\n              >\n                <span className=\"sr-only\">Última página</span>\n                <ChevronsRight className=\"h-4 w-4\" />\n              </Button>\n            </div>\n            <select\n              value={table.getState().pagination.pageSize}\n              onChange={e => {\n                table.setPageSize(Number(e.target.value))\n              }}\n              className=\"h-7 rounded-md border border-input bg-background px-2 py-1 text-xs text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n            >\n              {[20, 30, 40, 50].map(pageSize => (\n                <option key={pageSize} value={pageSize}>\n                  {pageSize} / pág\n                </option>\n              ))}\n            </select>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Tabela densa de admin ou dashboard com ordenação, seleção e edição inline",
      "evite": "Fluxo por etapas visível: kanban; lista curta sem ordenar nem editar, table"
    }
  },
  "categories": [
    "conteudo",
    "tabela",
    "fila",
    "tanstack"
  ],
  "type": "registry:ui"
}