{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "dependencies": [
    "@radix-ui/react-accordion",
    "@radix-ui/react-slot",
    "@radix-ui/react-tooltip",
    "class-variance-authority",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/watermelon-ui/data-table.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n\nexport const DEFAULT_LOCALE = \"en-US\" as const;\n\nfunction isNumericFormat(format?: FormatConfig): boolean {\n  const kind = format?.kind;\n  return (\n    kind === \"number\" ||\n    kind === \"currency\" ||\n    kind === \"percent\" ||\n    kind === \"delta\"\n  );\n}\n\nfunction getAlignmentClass(\n  align?: \"left\" | \"right\" | \"center\",\n): string | undefined {\n  if (align === \"right\") return \"text-right\";\n  if (align === \"center\") return \"text-center\";\n  return undefined;\n}\n\nconst DataTableContext = React.createContext<\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  DataTableContextValue<any> | undefined\n>(undefined);\n\nexport function useDataTable<T extends object = RowData>() {\n  const context = React.useContext(DataTableContext) as\n    | DataTableContextValue<T>\n    | undefined;\n  if (!context) {\n    throw new Error(\"useDataTable must be used within a DataTable\");\n  }\n  return context;\n}\n\nexport function DataTable<T extends object = RowData>({\n  columns,\n  data: rawData,\n  rowIdKey,\n  layout = \"auto\",\n  defaultSort,\n  sort: controlledSort,\n  emptyMessage = \"No data available\",\n  isLoading = false,\n  maxHeight,\n  id,\n  onSortChange,\n  className,\n  locale,\n  responseActions,\n  onResponseAction,\n  onBeforeResponseAction,\n}: DataTableProps<T>) {\n  // Default locale avoids SSR/client formatting mismatches.\n  const resolvedLocale = locale ?? DEFAULT_LOCALE;\n\n  const [internalSortBy, setInternalSortBy] = React.useState<\n    ColumnKey<T> | undefined\n  >(defaultSort?.by);\n  const [internalSortDirection, setInternalSortDirection] = React.useState<\n    \"asc\" | \"desc\" | undefined\n  >(defaultSort?.direction);\n\n  const sortBy = controlledSort?.by ?? internalSortBy;\n  const sortDirection = controlledSort?.direction ?? internalSortDirection;\n\n  const data = React.useMemo(() => {\n    if (!sortBy || !sortDirection) return rawData;\n    return sortData(rawData, sortBy, sortDirection, resolvedLocale);\n  }, [rawData, sortBy, sortDirection, resolvedLocale]);\n\n  const handleSort = React.useCallback(\n    (key: ColumnKey<T>) => {\n      let newDirection: \"asc\" | \"desc\" | undefined;\n\n      if (sortBy === key) {\n        if (sortDirection === \"asc\") {\n          newDirection = \"desc\";\n        } else if (sortDirection === \"desc\") {\n          newDirection = undefined;\n        } else {\n          newDirection = \"asc\";\n        }\n      } else {\n        newDirection = \"asc\";\n      }\n\n      const next = {\n        by: newDirection ? key : undefined,\n        direction: newDirection,\n      } as const;\n\n      if (controlledSort) {\n        onSortChange?.(next);\n      } else {\n        setInternalSortBy(next.by);\n        setInternalSortDirection(next.direction);\n      }\n    },\n    [sortBy, sortDirection, controlledSort, onSortChange],\n  );\n\n  const contextValue: DataTableContextValue<T> = {\n    columns,\n    data,\n    rowIdKey,\n    sortBy,\n    sortDirection,\n    toggleSort: handleSort,\n    id,\n    isLoading,\n    locale: resolvedLocale,\n  };\n\n  const sortAnnouncement = React.useMemo(() => {\n    const col = columns.find((c) => c.key === sortBy);\n    const label = col?.label ?? sortBy;\n    return sortBy && sortDirection\n      ? `Sorted by ${label}, ${sortDirection === \"asc\" ? \"ascending\" : \"descending\"}`\n      : \"\";\n  }, [columns, sortBy, sortDirection]);\n\n  const normalizedFooterActions = React.useMemo(\n    () => normalizeActionsConfig(responseActions),\n    [responseActions],\n  );\n\n  return (\n    <DataTableContext.Provider value={contextValue}>\n      <div\n        className={cn(\"@container w-full min-w-80\", className)}\n        data-tool-ui-id={id}\n        data-slot=\"data-table\"\n        data-layout={layout}\n      >\n        <div\n          className={cn(\n            layout === \"table\"\n              ? \"block\"\n              : layout === \"cards\"\n                ? \"hidden\"\n                : \"hidden @md:block\",\n          )}\n        >\n          <div className=\"relative\">\n            <div\n              className={cn(\n                \"bg-card relative w-full overflow-clip overflow-y-auto rounded-lg border\",\n                \"touch-pan-x\",\n                maxHeight && \"max-h-[--max-height]\",\n              )}\n              style={\n                maxHeight\n                  ? ({ \"--max-height\": maxHeight } as React.CSSProperties)\n                  : undefined\n              }\n            >\n              <DataTableErrorBoundary>\n                <Table aria-busy={isLoading || undefined}>\n                  {columns.length > 0 && (\n                    <colgroup>\n                      {columns.map((col) => (\n                        <col\n                          key={String(col.key)}\n                          style={col.width ? { width: col.width } : undefined}\n                        />\n                      ))}\n                    </colgroup>\n                  )}\n                  {isLoading ? (\n                    <DataTableSkeleton />\n                  ) : data.length === 0 ? (\n                    <DataTableEmpty message={emptyMessage} />\n                  ) : (\n                    <DataTableContent />\n                  )}\n                </Table>\n              </DataTableErrorBoundary>\n            </div>\n          </div>\n        </div>\n\n        <div\n          className={cn(\n            layout === \"cards\"\n              ? \"\"\n              : layout === \"table\"\n                ? \"hidden\"\n                : \"@md:hidden\",\n          )}\n          role=\"list\"\n          aria-label=\"Data table (mobile card view)\"\n          aria-describedby=\"mobile-table-description\"\n        >\n          <div id=\"mobile-table-description\" className=\"sr-only\">\n            Table data shown as expandable cards. Each card represents one row.\n            {columns.length > 0 &&\n              ` Columns: ${columns.map((c) => c.label).join(\", \")}.`}\n          </div>\n\n          <DataTableErrorBoundary>\n            {isLoading ? (\n              <DataTableSkeletonCards />\n            ) : data.length === 0 ? (\n              <div className=\"text-muted-foreground py-8 text-center\">\n                {emptyMessage}\n              </div>\n            ) : (\n              <div className=\"bg-card flex flex-col overflow-hidden rounded-2xl border shadow-xs\">\n                {data.map((row, i) => {\n                  const keyVal = rowIdKey ? row[rowIdKey] : undefined;\n                  const rowKey = keyVal != null ? String(keyVal) : String(i);\n                  return (\n                    <DataTableAccordionCard\n                      key={rowKey}\n                      row={row as unknown as DataTableRowData}\n                      index={i}\n                      isFirst={i === 0}\n                    />\n                  );\n                })}\n              </div>\n            )}\n          </DataTableErrorBoundary>\n        </div>\n\n        {sortAnnouncement && (\n          <div className=\"sr-only\" aria-live=\"polite\">\n            {sortAnnouncement}\n          </div>\n        )}\n\n        {normalizedFooterActions ? (\n          <div className=\"@container/actions mt-4\">\n            <ActionButtons\n              actions={normalizedFooterActions.items}\n              align={normalizedFooterActions.align}\n              confirmTimeout={normalizedFooterActions.confirmTimeout}\n              onAction={(id) => onResponseAction?.(id)}\n              onBeforeAction={onBeforeResponseAction}\n            />\n          </div>\n        ) : null}\n      </div>\n    </DataTableContext.Provider>\n  );\n}\n\nfunction DataTableContent() {\n  return (\n    <>\n      <DataTableHeader />\n      <DataTableBody />\n    </>\n  );\n}\n\nfunction DataTableEmpty({ message }: { message: string }) {\n  const { columns } = useDataTable();\n\n  return (\n    <TableBody>\n      <TableRow className=\"bg-card h-24 text-center\">\n        <TableCell colSpan={columns.length} role=\"status\" aria-live=\"polite\">\n          {message}\n        </TableCell>\n      </TableRow>\n    </TableBody>\n  );\n}\n\nfunction DataTableSkeleton() {\n  const { columns } = useDataTable();\n\n  return (\n    <>\n      <DataTableHeader />\n      <TableBody>\n        {Array.from({ length: 5 }).map((_, i) => (\n          <TableRow key={i}>\n            {columns.map((_, j) => (\n              <TableCell key={j}>\n                <div className=\"bg-muted/50 h-4 rounded motion-safe:animate-pulse\" />\n              </TableCell>\n            ))}\n          </TableRow>\n        ))}\n      </TableBody>\n    </>\n  );\n}\n\nfunction DataTableSkeletonCards() {\n  return (\n    <>\n      {Array.from({ length: 3 }).map((_, i) => (\n        <div key={i} className=\"flex flex-col gap-2 rounded-lg border p-4\">\n          <div className=\"bg-muted/50 h-5 w-1/2 rounded motion-safe:animate-pulse\" />\n          <div className=\"bg-muted/50 h-4 w-3/4 rounded motion-safe:animate-pulse\" />\n          <div className=\"bg-muted/50 h-4 w-2/3 rounded motion-safe:animate-pulse\" />\n        </div>\n      ))}\n    </>\n  );\n}\n\nfunction SortIcon({ state }: { state?: \"asc\" | \"desc\" }) {\n  let char = \"⇅\";\n  let className = \"opacity-20\";\n\n  if (state === \"asc\") {\n    char = \"↑\";\n    className = \"\";\n  }\n\n  if (state === \"desc\") {\n    char = \"↓\";\n    className = \"\";\n  }\n\n  return (\n    <span aria-hidden className={cn(\"min-w-4 shrink-0 text-center\", className)}>\n      {char}\n    </span>\n  );\n}\n\nfunction DataTableHeader() {\n  const { columns } = useDataTable();\n\n  return (\n    <TooltipProvider delayDuration={300}>\n      <TableHeader>\n        <TableRow className=\"hover:bg-transparent\">\n          {columns.map((column, columnIndex) => (\n            <DataTableHead\n              key={column.key}\n              column={column}\n              columnIndex={columnIndex}\n              totalColumns={columns.length}\n            />\n          ))}\n        </TableRow>\n      </TableHeader>\n    </TooltipProvider>\n  );\n}\n\ninterface DataTableHeadProps {\n  column: Column;\n  columnIndex?: number;\n  totalColumns?: number;\n}\n\nfunction DataTableHead({\n  column,\n  columnIndex = 0,\n  totalColumns = 1,\n}: DataTableHeadProps) {\n  const { sortBy, sortDirection, toggleSort, isLoading } = useDataTable();\n  const isFirstColumn = columnIndex === 0;\n  const isLastColumn = columnIndex === totalColumns - 1;\n\n  const isSortable = column.sortable !== false;\n\n  const isSorted = sortBy === column.key;\n  const direction = isSorted ? sortDirection : undefined;\n  const isDisabled = isLoading || !isSortable;\n\n  const handleClick = () => {\n    if (!isDisabled && toggleSort) {\n      toggleSort(column.key);\n    }\n  };\n\n  const displayText = column.abbr || column.label;\n  const shouldShowTooltip = column.abbr || displayText.length > 15;\n  const isNumericKind = isNumericFormat(column.format);\n  const align =\n    column.align ??\n    (columnIndex === 0 ? \"left\" : isNumericKind ? \"right\" : \"left\");\n  const alignClass = getAlignmentClass(align);\n  const buttonAlignClass = cn(\n    \"min-w-0 gap-1 font-normal\",\n    align === \"right\" && \"text-right\",\n    align === \"center\" && \"text-center\",\n    align === \"left\" && \"text-left\",\n  );\n  const labelAlignClass =\n    align === \"right\"\n      ? \"text-right\"\n      : align === \"center\"\n        ? \"text-center\"\n        : \"text-left\";\n\n  return (\n    <TableHead\n      scope=\"col\"\n      className={cn(\n        alignClass,\n        isFirstColumn && \"pl-1\",\n        isLastColumn && \"pr-1\",\n      )}\n      style={column.width ? { width: column.width } : undefined}\n      aria-sort={\n        isSorted\n          ? direction === \"asc\"\n            ? \"ascending\"\n            : \"descending\"\n          : undefined\n      }\n    >\n      <Button\n        type=\"button\"\n        size=\"sm\"\n        onClick={handleClick}\n        onKeyDown={(e) => {\n          if (isDisabled) return;\n          if (e.key === \"Enter\" || e.key === \" \") {\n            e.preventDefault();\n            handleClick();\n          }\n        }}\n        disabled={isDisabled}\n        variant=\"ghost\"\n        className={cn(\n          buttonAlignClass,\n          \"w-fit min-w-10\",\n          isFirstColumn && \"pl-4\",\n          isLastColumn && \"pr-4\",\n        )}\n        aria-label={\n          `Sort by ${column.label}` +\n          (isSorted && direction\n            ? ` (${direction === \"asc\" ? \"ascending\" : \"descending\"})`\n            : \"\")\n        }\n        aria-disabled={isDisabled || undefined}\n      >\n        {shouldShowTooltip ? (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <span className={cn(\"truncate\", labelAlignClass)}>\n                {column.abbr ? (\n                  <abbr\n                    title={column.label}\n                    className={cn(\n                      \"cursor-help border-b border-dotted border-current no-underline\",\n                      labelAlignClass,\n                    )}\n                  >\n                    {column.abbr}\n                  </abbr>\n                ) : (\n                  <span className={labelAlignClass}>{column.label}</span>\n                )}\n              </span>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{column.label}</p>\n            </TooltipContent>\n          </Tooltip>\n        ) : (\n          <span className={cn(\"truncate\", labelAlignClass)}>\n            {column.label}\n          </span>\n        )}\n        {isSortable && <SortIcon state={direction} />}\n      </Button>\n    </TableHead>\n  );\n}\n\nfunction DataTableBody() {\n  const { data, rowIdKey } = useDataTable<DataTableRowData>();\n  const hasWarnedRowKeyRef = React.useRef(false);\n\n  React.useEffect(() => {\n    if (hasWarnedRowKeyRef.current) return;\n    if (process.env.NODE_ENV !== \"production\" && !rowIdKey && data.length > 0) {\n      hasWarnedRowKeyRef.current = true;\n      console.warn(\n        \"[DataTable] Missing `rowIdKey` prop. Using array index as React key can cause reconciliation issues when data reorders (focus traps, animation glitches, incorrect state preservation). \" +\n        \"Strongly recommended: Pass a `rowIdKey` prop that points to a unique identifier in your row data (e.g., 'id', 'uuid', 'symbol').\\n\" +\n        'Example: <DataTable rowIdKey=\"id\" columns={...} data={...} />',\n      );\n    }\n  }, [rowIdKey, data.length]);\n\n  return (\n    <TableBody>\n      {data.map((row, index) => {\n        const keyVal = rowIdKey ? row[rowIdKey] : undefined;\n        const rowKey = keyVal != null ? String(keyVal) : String(index);\n        return <DataTableRow key={rowKey} row={row} />;\n      })}\n    </TableBody>\n  );\n}\n\ninterface DataTableRowProps {\n  row: DataTableRowData;\n  className?: string;\n}\n\nfunction DataTableRow({ row, className }: DataTableRowProps) {\n  const { columns } = useDataTable();\n\n  return (\n    <TableRow className={className}>\n      {columns.map((column, columnIndex) => (\n        <DataTableCell\n          key={column.key}\n          value={row[column.key]}\n          column={column}\n          row={row}\n          columnIndex={columnIndex}\n        />\n      ))}\n    </TableRow>\n  );\n}\n\ninterface DataTableCellProps {\n  value:\n  | string\n  | number\n  | boolean\n  | null\n  | (string | number | boolean | null)[];\n  column: Column;\n  row: DataTableRowData;\n  className?: string;\n  columnIndex?: number;\n}\n\nfunction DataTableCell({\n  value,\n  column,\n  row,\n  className,\n  columnIndex = 0,\n}: DataTableCellProps) {\n  const { locale } = useDataTable();\n  const isNumericKind = isNumericFormat(column.format);\n  const isNumericValue = typeof value === \"number\";\n  const displayValue = renderFormattedValue({ value, column, row, locale });\n  const align =\n    column.align ??\n    (columnIndex === 0\n      ? \"left\"\n      : isNumericKind || isNumericValue\n        ? \"right\"\n        : \"left\");\n  const alignClass = getAlignmentClass(align);\n\n  return (\n    <TableCell className={cn(\"px-5 py-3\", alignClass, className)}>\n      {displayValue}\n    </TableCell>\n  );\n}\n\nfunction categorizeColumns(columns: Column[]) {\n  const primary: Column[] = [];\n  const secondary: Column[] = [];\n\n  let visibleColumnCount = 0;\n  columns.forEach((col) => {\n    if (col.hideOnMobile) return;\n\n    if (col.priority === \"primary\") {\n      primary.push(col);\n    } else if (col.priority === \"secondary\") {\n      secondary.push(col);\n    } else if (col.priority === \"tertiary\") {\n      return;\n    } else {\n      if (visibleColumnCount < 2) {\n        primary.push(col);\n      } else {\n        secondary.push(col);\n      }\n      visibleColumnCount++;\n    }\n  });\n\n  return { primary, secondary };\n}\n\ninterface DataTableAccordionCardProps {\n  row: DataTableRowData;\n  index: number;\n  isFirst?: boolean;\n}\n\nfunction DataTableAccordionCard({\n  row,\n  index,\n  isFirst = false,\n}: DataTableAccordionCardProps) {\n  const { columns, locale, rowIdKey } = useDataTable();\n\n  const { primary, secondary } = React.useMemo(\n    () => categorizeColumns(columns),\n    [columns],\n  );\n\n  if (secondary.length === 0) {\n    return (\n      <SimpleCard row={row} columns={primary} index={index} isFirst={isFirst} />\n    );\n  }\n\n  const primaryColumn = primary[0];\n  const remainingPrimaryColumns = primary.slice(1);\n\n  const stableRowId =\n    getRowIdentifier(row, rowIdKey ? String(rowIdKey) : undefined) ||\n    `${index}-${primaryColumn?.key ?? \"row\"}`;\n\n  const headingId = `row-${stableRowId}-heading`;\n  const detailsId = `row-${stableRowId}-details`;\n  const remainingPrimaryDataIds = remainingPrimaryColumns.map(\n    (col) => `row-${stableRowId}-${String(col.key)}`,\n  );\n\n  const primaryValue = primaryColumn\n    ? String(row[primaryColumn.key] ?? \"\")\n    : \"\";\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\n  const accordionItemId = `row-${stableRowId}`;\n\n  return (\n    <Accordion\n      type=\"single\"\n      collapsible\n      className={cn(!isFirst && \"border-t\")}\n      role=\"listitem\"\n      aria-label={rowLabel}\n    >\n      <AccordionItem value={accordionItemId} className=\"group border-0\">\n        <AccordionTrigger\n          className=\"group-data-[state=closed]:hover:bg-accent/50 active:bg-accent/50 group-data-[state=open]:bg-muted w-full rounded-none px-4 py-3 hover:no-underline\"\n          aria-controls={detailsId}\n          aria-label={`${rowLabel}. ${secondary.length > 0 ? \"Expand for details\" : \"\"}`}\n        >\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n            {primaryColumn && (\n              <div\n                id={headingId}\n                role=\"heading\"\n                aria-level={3}\n                className=\"truncate\"\n                aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\n              >\n                {renderFormattedValue({\n                  value: row[primaryColumn.key],\n                  column: primaryColumn,\n                  row,\n                  locale,\n                })}\n              </div>\n            )}\n\n            {remainingPrimaryColumns.length > 0 && (\n              <div\n                className=\"text-muted-foreground flex w-full flex-wrap gap-x-4 gap-y-0.5\"\n                role=\"group\"\n                aria-label=\"Summary information\"\n              >\n                {remainingPrimaryColumns.map((col, idx) => (\n                  <span\n                    key={col.key}\n                    id={remainingPrimaryDataIds[idx]}\n                    className=\"flex min-w-0 gap-1 font-normal\"\n                    role=\"cell\"\n                    aria-label={`${col.label}: ${row[col.key]}`}\n                  >\n                    <span className=\"sr-only\">{col.label}:</span>\n                    <span aria-hidden=\"true\">{col.label}:</span>\n                    <span className=\"truncate\">\n                      {renderFormattedValue({\n                        value: row[col.key],\n                        column: col,\n                        row,\n                        locale,\n                      })}\n                    </span>\n                  </span>\n                ))}\n              </div>\n            )}\n          </div>\n        </AccordionTrigger>\n\n        <AccordionContent\n          className={\"flex flex-col gap-4 px-4 pb-4\"}\n          id={detailsId}\n          role=\"region\"\n          aria-labelledby={headingId}\n        >\n          {secondary.length > 0 && (\n            <dl\n              className={cn(\n                \"flex flex-col gap-2 pt-4\",\n                \"motion-safe:group-data-[state=open]:animate-in motion-safe:group-data-[state=open]:fade-in-0\",\n                \"motion-safe:group-data-[state=open]:slide-in-from-top-1\",\n                \"motion-safe:group-data-[state=closed]:animate-out motion-safe:group-data-[state=closed]:fade-out-0\",\n                \"motion-safe:group-data-[state=closed]:slide-out-to-top-1\",\n                \"duration-150\",\n              )}\n              role=\"list\"\n              aria-label=\"Additional data\"\n            >\n              {secondary.map((col) => (\n                <div\n                  key={col.key}\n                  className=\"flex items-start justify-between gap-4\"\n                  role=\"listitem\"\n                >\n                  <dt\n                    className=\"text-muted-foreground shrink-0\"\n                    id={`row-${stableRowId}-${String(col.key)}-label`}\n                  >\n                    {col.label}\n                  </dt>\n                  <dd\n                    className={cn(\n                      \"text-foreground min-w-0 text-pretty wrap-break-word\",\n                      col.align === \"right\" && \"text-right\",\n                      col.align === \"center\" && \"text-center\",\n                    )}\n                    role=\"cell\"\n                    aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\n                  >\n                    {renderFormattedValue({\n                      value: row[col.key],\n                      column: col,\n                      row,\n                      locale,\n                    })}\n                  </dd>\n                </div>\n              ))}\n            </dl>\n          )}\n        </AccordionContent>\n      </AccordionItem>\n    </Accordion>\n  );\n}\n\n/**\n * Simple card with no accordion,   for when there are only primary columns\n */\nfunction SimpleCard({\n  row,\n  columns,\n  index,\n  isFirst = false,\n}: {\n  row: DataTableRowData;\n  columns: Column[];\n  index: number;\n  isFirst?: boolean;\n}) {\n  const { locale, rowIdKey } = useDataTable();\n  const primaryColumn = columns[0];\n  const otherColumns = columns.slice(1);\n\n  const stableRowId =\n    getRowIdentifier(row, rowIdKey ? String(rowIdKey) : undefined) ||\n    `${index}-${primaryColumn?.key ?? \"row\"}`;\n\n  const primaryValue = primaryColumn\n    ? String(row[primaryColumn.key] ?? \"\")\n    : \"\";\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2 p-4\", !isFirst && \"border-t\")}\n      role=\"listitem\"\n      aria-label={rowLabel}\n    >\n      {primaryColumn && (\n        <div\n          role=\"heading\"\n          aria-level={3}\n          aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\n        >\n          {renderFormattedValue({\n            value: row[primaryColumn.key],\n            column: primaryColumn,\n            row,\n            locale,\n          })}\n        </div>\n      )}\n\n      {otherColumns.map((col) => (\n        <div\n          key={col.key}\n          className=\"flex items-start justify-between gap-4\"\n          role=\"group\"\n        >\n          <span\n            className=\"text-muted-foreground\"\n            id={`row-${stableRowId}-${String(col.key)}-label`}\n          >\n            {col.label}:\n          </span>\n          <span\n            className={cn(\n              \"min-w-0 wrap-break-word\",\n              col.align === \"right\" && \"text-right\",\n              col.align === \"center\" && \"text-center\",\n            )}\n            role=\"cell\"\n            aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\n          >\n            {renderFormattedValue({\n              value: row[col.key],\n              column: col,\n              row,\n              locale,\n            })}\n          </span>\n        </div>\n      ))}\n    </div>\n  );\n}\n\n\nexport function DataTableErrorBoundary(\n  props: Omit<ToolUIErrorBoundaryProps, \"componentName\">,\n) {\n  const { children, ...rest } = props;\n  return (\n    <ToolUIErrorBoundary componentName=\"DataTable\" {...rest}>\n      {children}\n    </ToolUIErrorBoundary>\n  );\n}\n\n\n\n\nexport interface ToolUIErrorBoundaryProps {\n  componentName: string;\n  children: React.ReactNode;\n  fallback?: React.ReactNode;\n  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n}\n\ninterface ToolUIErrorBoundaryState {\n  hasError: boolean;\n  error?: Error;\n}\n\nexport class ToolUIErrorBoundary extends React.Component<\n  ToolUIErrorBoundaryProps,\n  ToolUIErrorBoundaryState\n> {\n  constructor(props: ToolUIErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(error: Error): ToolUIErrorBoundaryState {\n    return { hasError: true, error };\n  }\n\n  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n    console.error(`[${this.props.componentName}] render error:`, error, errorInfo);\n    this.props.onError?.(error, errorInfo);\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return (\n        this.props.fallback ?? (\n          <div className=\"border-destructive text-destructive rounded-lg border p-4\">\n            <p className=\"font-semibold\">\n              {this.props.componentName} failed to render\n            </p>\n            <p className=\"text-sm\">{this.state.error?.message}</p>\n          </div>\n        )\n      );\n    }\n    return this.props.children;\n  }\n}\n\n\n\ntype Tone = \"success\" | \"warning\" | \"danger\" | \"info\" | \"neutral\";\n\nexport type FormatConfig =\n  | { kind: \"text\" }\n  | {\n    kind: \"number\";\n    decimals?: number;\n    unit?: string;\n    compact?: boolean;\n    showSign?: boolean;\n  }\n  | { kind: \"currency\"; currency: string; decimals?: number }\n  | {\n    kind: \"percent\";\n    decimals?: number;\n    showSign?: boolean;\n    basis?: \"fraction\" | \"unit\";\n  }\n  | { kind: \"date\"; dateFormat?: \"short\" | \"long\" | \"relative\" }\n  | {\n    kind: \"delta\";\n    decimals?: number;\n    upIsPositive?: boolean;\n    showSign?: boolean;\n  }\n  | {\n    kind: \"status\";\n    statusMap: Record<string, { tone: Tone; label?: string }>;\n  }\n  | { kind: \"boolean\"; labels?: { true: string; false: string } }\n  | { kind: \"link\"; hrefKey?: string; external?: boolean }\n  | { kind: \"badge\"; colorMap?: Record<string, Tone> }\n  | { kind: \"array\"; maxVisible?: number };\n\ninterface DeltaValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"delta\" }>;\n}\n\nexport function DeltaValue({ value, options }: DeltaValueProps) {\n  const decimals = options?.decimals ?? 2;\n  const upIsPositive = options?.upIsPositive ?? true;\n  const showSign = options?.showSign ?? true;\n\n  const isPositive = value > 0;\n  const isNegative = value < 0;\n  const isNeutral = value === 0;\n\n  const isGood = upIsPositive ? isPositive : isNegative;\n  const isBad = upIsPositive ? isNegative : isPositive;\n\n  const colorClass = isGood\n    ? \"text-green-700 dark:text-green-500\"\n    : isBad\n      ? \"text-destructive\"\n      : \"text-muted-foreground\";\n\n  const formatted = value.toFixed(decimals);\n  const display = showSign && !isNegative ? `+${formatted}` : formatted;\n\n  const arrow = isPositive ? \"↑\" : isNegative ? \"↓\" : \"\";\n\n  return (\n    <span className={cn(\"tabular-nums\", colorClass)}>\n      {display}\n      {!isNeutral && <span className=\"ml-0.5\">{arrow}</span>}\n    </span>\n  );\n}\n\ninterface StatusBadgeProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"status\" }>;\n}\n\nexport function StatusBadge({ value, options }: StatusBadgeProps) {\n  const config = options?.statusMap?.[value] ?? {\n    tone: \"neutral\" as Tone,\n    label: value,\n  };\n  const label = config.label ?? value;\n\n  const variant =\n    config.tone === \"danger\"\n      ? \"destructive\"\n      : config.tone === \"neutral\"\n        ? \"outline\"\n        : \"secondary\";\n\n  return (\n    <Badge\n      variant={variant}\n      className={cn(\n        \"border\",\n        config.tone === \"warning\" &&\n        \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\",\n        config.tone === \"success\" &&\n        \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\",\n        config.tone === \"info\" &&\n        \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\",\n        config.tone === \"danger\" &&\n        \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\",\n      )}\n    >\n      {label}\n    </Badge>\n  );\n}\n\ninterface CurrencyValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"currency\" }>;\n  locale?: string;\n}\n\nexport function CurrencyValue({ value, options, locale }: CurrencyValueProps) {\n  const currency = options?.currency ?? \"USD\";\n  const decimals = options?.decimals ?? 2;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  }).format(value);\n\n  return <span className=\"tabular-nums\">{formatted}</span>;\n}\n\ninterface PercentValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"percent\" }>;\n}\n\nexport function PercentValue({ value, options }: PercentValueProps) {\n  const decimals = options?.decimals ?? 2;\n  const showSign = options?.showSign ?? false;\n  const basis = options?.basis ?? \"fraction\";\n\n  const numeric = basis === \"fraction\" ? value * 100 : value;\n  const absoluteFormatted = Math.abs(numeric).toFixed(decimals);\n  const signed =\n    numeric > 0 && showSign\n      ? `+${absoluteFormatted}`\n      : numeric < 0\n        ? `-${absoluteFormatted}`\n        : absoluteFormatted;\n\n  return <span className=\"tabular-nums\">{signed}%</span>;\n}\n\ninterface DateValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"date\" }>;\n  locale?: string;\n}\n\nexport function DateValue({ value, options, locale }: DateValueProps) {\n  const dateFormat = options?.dateFormat ?? \"short\";\n  const date = new Date(value);\n\n  if (isNaN(date.getTime())) {\n    return <span>Invalid date</span>;\n  }\n\n  let formatted: string;\n\n  if (dateFormat === \"relative\") {\n    formatted = getRelativeTime(date, locale);\n  } else if (dateFormat === \"long\") {\n    formatted = new Intl.DateTimeFormat(locale, {\n      year: \"numeric\",\n      month: \"long\",\n      day: \"numeric\",\n    }).format(date);\n  } else {\n    formatted = new Intl.DateTimeFormat(locale, {\n      year: \"numeric\",\n      month: \"short\",\n      day: \"numeric\",\n    }).format(date);\n  }\n\n  const title = new Intl.DateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n  }).format(date);\n\n  return (\n    <span className=\"tabular-nums\" title={title}>\n      {formatted}\n    </span>\n  );\n}\n\nfunction getRelativeTime(date: Date, locale?: string): string {\n  const now = new Date();\n  const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);\n\n  if (diffInSeconds < 60) return \"just now\";\n  if (diffInSeconds < 3600) {\n    const mins = Math.floor(diffInSeconds / 60);\n    return `${mins} ${mins === 1 ? \"minute\" : \"minutes\"} ago`;\n  }\n  if (diffInSeconds < 86400) {\n    const hours = Math.floor(diffInSeconds / 3600);\n    return `${hours} ${hours === 1 ? \"hour\" : \"hours\"} ago`;\n  }\n  if (diffInSeconds < 604800) {\n    const days = Math.floor(diffInSeconds / 86400);\n    return `${days} ${days === 1 ? \"day\" : \"days\"} ago`;\n  }\n\n  return new Intl.DateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  }).format(date);\n}\n\ninterface BooleanValueProps {\n  value: boolean;\n  options?: Extract<FormatConfig, { kind: \"boolean\" }>;\n}\n\nexport function BooleanValue({ value, options }: BooleanValueProps) {\n  const labels = options?.labels ?? { true: \"Yes\", false: \"No\" };\n  const label = value ? labels.true : labels.false;\n  const variant = value ? \"secondary\" : \"outline\";\n\n  return <Badge variant={variant}>{label}</Badge>;\n}\n\ninterface LinkValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"link\" }>;\n  row?: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >;\n}\n\nexport function LinkValue({ value, options, row }: LinkValueProps) {\n  const href =\n    options?.hrefKey && row ? String(row[options.hrefKey] ?? \"\") : value;\n  const external = options?.external ?? false;\n\n  if (!href) {\n    return <span>{value}</span>;\n  }\n\n  return (\n    <a\n      href={href}\n      target={external ? \"_blank\" : undefined}\n      rel={external ? \"noopener noreferrer\" : undefined}\n      className=\"text-accent-foreground inline-block max-w-full break-words underline underline-offset-2 hover:opacity-90\"\n      aria-label={external ? `${value} (opens in a new tab)` : undefined}\n      onClick={(e) => e.stopPropagation()}\n    >\n      {value}\n      {external && (\n        <span className=\"ml-1 inline-block\" aria-label=\"Opens in new tab\">\n          ↗\n        </span>\n      )}\n    </a>\n  );\n}\n\ninterface NumberValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"number\" }>;\n  locale?: string;\n}\n\nexport function NumberValue({ value, options, locale }: NumberValueProps) {\n  const decimals = options?.decimals ?? 0;\n  const unit = options?.unit ?? \"\";\n  const compact = options?.compact ?? false;\n  const showSign = options?.showSign ?? false;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n    notation: compact ? \"compact\" : \"standard\",\n  }).format(value);\n\n  const display = showSign && value > 0 ? `+${formatted}` : formatted;\n\n  return (\n    <span className=\"tabular-nums\">\n      {display}\n      {unit}\n    </span>\n  );\n}\n\ninterface BadgeValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"badge\" }>;\n}\n\nexport function BadgeValue({ value, options }: BadgeValueProps) {\n  const tone = options?.colorMap?.[value] ?? \"neutral\";\n\n  const variant =\n    tone === \"danger\"\n      ? \"destructive\"\n      : tone === \"neutral\"\n        ? \"outline\"\n        : \"secondary\";\n\n  return (\n    <Badge\n      variant={variant}\n      className={cn(\n        \"border\",\n        tone === \"warning\" &&\n        \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\",\n        tone === \"success\" &&\n        \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\",\n        tone === \"info\" &&\n        \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\",\n        tone === \"danger\" &&\n        \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\",\n      )}\n    >\n      {value}\n    </Badge>\n  );\n}\n\ninterface ArrayValueProps {\n  value: (string | number | boolean | null)[] | string;\n  options?: Extract<FormatConfig, { kind: \"array\" }>;\n}\n\nexport function ArrayValue({ value, options }: ArrayValueProps) {\n  const maxVisible = options?.maxVisible ?? 3;\n  const items: (string | number | boolean | null)[] = Array.isArray(value)\n    ? value\n    : typeof value === \"string\"\n      ? value.split(\",\").map((s) => s.trim())\n      : [];\n\n  if (items.length === 0) {\n    return <span className=\"text-muted\">—</span>;\n  }\n\n  const visible = items.slice(0, maxVisible);\n  const remaining = items.length - maxVisible;\n\n  const hidden = items.slice(maxVisible);\n\n  return (\n    <span className=\"inline-flex flex-wrap items-center gap-1\">\n      {visible.map((item, i) => (\n        <span\n          key={i}\n          className=\"bg-muted text-muted-foreground inline-flex items-center rounded-md px-2 py-0.5\"\n        >\n          {item === null ? \"null\" : String(item)}\n        </span>\n      ))}\n      {remaining > 0 && (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <span className=\"text-muted-foreground cursor-default\">\n              +{remaining} more\n            </span>\n          </TooltipTrigger>\n          <TooltipContent>\n            {hidden\n              .map((item) => (item === null ? \"null\" : String(item)))\n              .join(\", \")}\n          </TooltipContent>\n        </Tooltip>\n      )}\n    </span>\n  );\n}\n\ninterface RenderFormattedValueParams {\n  value:\n  | string\n  | number\n  | boolean\n  | null\n  | (string | number | boolean | null)[];\n  column: { format?: FormatConfig };\n  row?: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >;\n  locale?: string;\n}\n\nexport function renderFormattedValue({\n  value,\n  column,\n  row,\n  locale,\n}: RenderFormattedValueParams): React.ReactNode {\n  if (value == null || value === \"\") {\n    return <span className=\"text-muted\">—</span>;\n  }\n\n  const fmt = column.format;\n\n  switch (fmt?.kind) {\n    case \"delta\":\n      return <DeltaValue value={Number(value)} options={fmt} />;\n    case \"status\":\n      return <StatusBadge value={String(value)} options={fmt} />;\n    case \"currency\":\n      return (\n        <CurrencyValue value={Number(value)} options={fmt} locale={locale} />\n      );\n    case \"percent\":\n      return <PercentValue value={Number(value)} options={fmt} />;\n    case \"date\":\n      return <DateValue value={String(value)} options={fmt} locale={locale} />;\n    case \"boolean\":\n      return <BooleanValue value={Boolean(value)} options={fmt} />;\n    case \"link\":\n      return <LinkValue value={String(value)} options={fmt} row={row} />;\n    case \"number\":\n      return (\n        <NumberValue value={Number(value)} options={fmt} locale={locale} />\n      );\n    case \"badge\":\n      return <BadgeValue value={String(value)} options={fmt} />;\n    case \"array\":\n      return (\n        <ArrayValue\n          value={Array.isArray(value) ? value : String(value)}\n          options={fmt}\n        />\n      );\n    case \"text\":\n    default:\n      return String(value);\n  }\n}\n\n\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  )\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  )\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        warning:\n          \"border-transparent bg-amber-500/10 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 [a&]:hover:bg-amber-500/20 dark:[a&]:hover:bg-amber-500/30\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n\n\nexport type ActionsProp = ActionsConfig | Action[];\n\nconst NEGATORY_ACTION_IDS = new Set([\n  \"cancel\",\n  \"dismiss\",\n  \"skip\",\n  \"no\",\n  \"reset\",\n  \"close\",\n  \"decline\",\n  \"reject\",\n  \"back\",\n  \"later\",\n  \"not-now\",\n  \"maybe-later\",\n]);\n\nfunction inferVariant(action: Action): Action {\n  if (action.variant) return action;\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\n    return { ...action, variant: \"ghost\" };\n  }\n  return action;\n}\n\nexport function normalizeActionsConfig(\n  actions?: ActionsProp,\n): ActionsConfig | null {\n  if (!actions) return null;\n\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\n\n  if (rawItems.length === 0) {\n    return null;\n  }\n\n  const items = rawItems.map(inferVariant);\n\n  return Array.isArray(actions)\n    ? { items }\n    : {\n      items,\n      align: actions.align,\n      confirmTimeout: actions.confirmTimeout,\n    };\n}\n\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)`\n * - Actions: `responseActions`, `onResponseAction`, `onBeforeResponseAction`\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n\n\nimport { z } from \"zod\";\n\nconst AlignEnum = z.enum([\"left\", \"right\", \"center\"]);\nconst PriorityEnum = z.enum([\"primary\", \"secondary\", \"tertiary\"]);\nconst LayoutEnum = z.enum([\"auto\", \"table\", \"cards\"]);\n\nconst formatSchema = z.discriminatedUnion(\"kind\", [\n  z.object({ kind: z.literal(\"text\") }),\n  z.object({\n    kind: z.literal(\"number\"),\n    decimals: z.number().optional(),\n    unit: z.string().optional(),\n    compact: z.boolean().optional(),\n    showSign: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"currency\"),\n    currency: z.string(),\n    decimals: z.number().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"percent\"),\n    decimals: z.number().optional(),\n    showSign: z.boolean().optional(),\n    basis: z.enum([\"fraction\", \"unit\"]).optional(),\n  }),\n  z.object({\n    kind: z.literal(\"date\"),\n    dateFormat: z.enum([\"short\", \"long\", \"relative\"]).optional(),\n  }),\n  z.object({\n    kind: z.literal(\"delta\"),\n    decimals: z.number().optional(),\n    upIsPositive: z.boolean().optional(),\n    showSign: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"status\"),\n    statusMap: z.record(\n      z.string(),\n      z.object({\n        tone: z.enum([\"success\", \"warning\", \"danger\", \"info\", \"neutral\"]),\n        label: z.string().optional(),\n      }),\n    ),\n  }),\n  z.object({\n    kind: z.literal(\"boolean\"),\n    labels: z\n      .object({\n        true: z.string(),\n        false: z.string(),\n      })\n      .optional(),\n  }),\n  z.object({\n    kind: z.literal(\"link\"),\n    hrefKey: z.string().optional(),\n    external: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"badge\"),\n    colorMap: z\n      .record(\n        z.string(),\n        z.enum([\"success\", \"warning\", \"danger\", \"info\", \"neutral\"]),\n      )\n      .optional(),\n  }),\n  z.object({\n    kind: z.literal(\"array\"),\n    maxVisible: z.number().optional(),\n  }),\n]);\n\nexport const serializableColumnSchema = z.object({\n  key: z.string(),\n  label: z.string(),\n  abbr: z.string().optional(),\n  sortable: z.boolean().optional(),\n  align: AlignEnum.optional(),\n  width: z.string().optional(),\n  truncate: z.boolean().optional(),\n  priority: PriorityEnum.optional(),\n  hideOnMobile: z.boolean().optional(),\n  format: formatSchema.optional(),\n});\n\nconst JsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);\n\n/**\n * Schema for serializable row data.\n *\n * Supports:\n * - Primitives: string, number, boolean, null\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\n *\n * Does NOT support:\n * - Functions\n * - Class instances (Date, Map, Set, etc.)\n * - Plain objects (use format configs instead)\n *\n * @example\n * Valid row data:\n * ```json\n * {\n *   \"name\": \"Widget\",\n *   \"price\": 29.99,\n *   \"active\": true,\n *   \"tags\": [\"electronics\", \"featured\"],\n *   \"metrics\": [1.2, 3.4, 5.6],\n *   \"flags\": [true, false, true],\n *   \"mixed\": [\"label\", 42, true]\n * }\n * ```\n */\nexport const serializableDataSchema = z.record(\n  z.string(),\n  z.union([JsonPrimitiveSchema, z.array(JsonPrimitiveSchema)]),\n);\n\n/**\n * Zod schema for validating DataTable payloads from LLM tool calls.\n *\n * This schema validates the serializable parts of a DataTable:\n * - id: Unique identifier for this tool UI in the conversation\n * - columns: Column definitions (keys, labels, formatting, etc.)\n * - data: Data rows (primitives only - no functions or class instances)\n * - layout: Optional layout override ('auto' | 'table' | 'cards')\n *\n * Non-serializable props like `onSortChange`, `className`, and `isLoading`\n * must be provided separately in your React component.\n *\n * @example\n * ```ts\n * const result = SerializableDataTableSchema.safeParse(llmResponse)\n * if (result.success) {\n *   // result.data contains validated id, columns, and data\n * }\n * ```\n */\nexport const SerializableDataTableSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  columns: z.array(serializableColumnSchema),\n  data: z.array(serializableDataSchema),\n  layout: LayoutEnum.optional(),\n});\n\n/**\n * Type representing the serializable parts of a DataTable payload.\n *\n * This type includes only JSON-serializable data that can come from LLM tool calls:\n * - Column definitions (format configs, alignment, labels, etc.)\n * - Row data (primitives: strings, numbers, booleans, null, string arrays)\n *\n * Excluded from this type:\n * - Event handlers (`onSortChange`, `onResponseAction`)\n * - React-specific props (`className`, `isLoading`, `responseActions`)\n *\n * @example\n * ```ts\n * const payload: SerializableDataTable = {\n *   id: \"data-table-expenses\",\n *   columns: [\n *     { key: \"name\", label: \"Name\" },\n *     { key: \"price\", label: \"Price\", format: { kind: \"currency\", currency: \"USD\" } }\n *   ],\n *   data: [\n *     { name: \"Widget\", price: 29.99 }\n *   ]\n * }\n * ```\n */\nexport type SerializableDataTable = z.infer<typeof SerializableDataTableSchema>;\n\n/**\n * Validates and parses a DataTable payload from unknown data (e.g., LLM tool call result).\n *\n * This function:\n * 1. Validates the input against the `SerializableDataTableSchema`\n * 2. Throws a descriptive error if validation fails\n * 3. Returns typed serializable props ready to pass to the `<DataTable>` component\n *\n * The returned props are **serializable only** - you must provide client-side props\n * separately (onSortChange, isLoading, className, responseActions, onResponseAction).\n *\n * @param input - Unknown data to validate (typically from an LLM tool call)\n * @returns Validated and typed DataTable serializable props (id, columns, data)\n * @throws Error with validation details if input is invalid\n *\n * @example\n * ```tsx\n * function MyToolUI({ result }: { result: unknown }) {\n *   const serializableProps = parseSerializableDataTable(result)\n *\n *   return (\n *     <DataTable\n *       {...serializableProps}\n *       responseActions={[{ id: \"export\", label: \"Export\" }]}\n *       onResponseAction={(id) => console.log(id)}\n *     />\n *   )\n * }\n * ```\n */\nexport function parseSerializableDataTable(\n  input: unknown,\n): Pick<\n  DataTableProps<RowData>,\n  \"id\" | \"role\" | \"receipt\" | \"columns\" | \"data\" | \"layout\"\n> {\n  const { id, role, receipt, columns, data, layout } = parseWithSchema(\n    SerializableDataTableSchema,\n    input,\n    \"DataTable\",\n  );\n  return {\n    id,\n    role,\n    receipt,\n    columns: columns as unknown as Column<RowData>[],\n    data: data as RowData[],\n    layout,\n  };\n}\n\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n\nexport interface ActionButtonsProps {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  className?: string;\n}\n\nexport function ActionButtons({\n  actions,\n  onAction,\n  onBeforeAction,\n  confirmTimeout = 3000,\n  align = \"right\",\n  className,\n}: ActionButtonsProps) {\n  const { actions: resolvedActions, runAction } = useActionButtons({\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout,\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-3\",\n        \"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\",\n        align === \"left\" && \"@sm/actions:justify-start\",\n        align === \"center\" && \"@sm/actions:justify-center\",\n        align === \"right\" && \"@sm/actions:justify-end\",\n        className,\n      )}\n    >\n      {resolvedActions.map((action) => {\n        const label = action.currentLabel;\n        const variant = action.variant || \"default\";\n\n        return (\n          <Button\n            key={action.id}\n            variant={variant}\n            onClick={() => runAction(action.id)}\n            disabled={action.isDisabled}\n            className={cn(\n              \"rounded-full px-4!\",\n              \"justify-center\",\n              \"min-h-11 w-full text-base\",\n              \"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\",\n              action.isConfirming &&\n              \"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\",\n            )}\n            aria-label={\n              action.shortcut ? `${label} (${action.shortcut})` : label\n            }\n          >\n            {action.isLoading && (\n              <svg\n                className=\"mr-2 h-4 w-4 motion-safe:animate-spin\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n              >\n                <circle\n                  className=\"opacity-25\"\n                  cx=\"12\"\n                  cy=\"12\"\n                  r=\"10\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"4\"\n                />\n                <path\n                  className=\"opacity-75\"\n                  fill=\"currentColor\"\n                  d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                />\n              </svg>\n            )}\n            {action.icon && !action.isLoading && (\n              <span className=\"mr-2\">{action.icon}</span>\n            )}\n            {label}\n            {action.shortcut && !action.isLoading && (\n              <kbd className=\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\">\n                {action.shortcut}\n              </kbd>\n            )}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\n\nexport type UseActionButtonsOptions = {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n};\n\nexport type UseActionButtonsResult = {\n  actions: Array<\n    Action & {\n      currentLabel: string;\n      isConfirming: boolean;\n      isExecuting: boolean;\n      isDisabled: boolean;\n      isLoading: boolean;\n    }\n  >;\n  runAction: (actionId: string) => Promise<void>;\n  confirmingActionId: string | null;\n  executingActionId: string | null;\n};\n\nexport function useActionButtons(\n  options: UseActionButtonsOptions,\n): UseActionButtonsResult {\n  const {\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout = 3000,\n  } = options;\n\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\n    null,\n  );\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\n    null,\n  );\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\n    return () => clearTimeout(id);\n  }, [confirmingActionId, confirmTimeout]);\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setConfirmingActionId(null);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [confirmingActionId]);\n\n  const runAction = useCallback(\n    async (actionId: string) => {\n      const action = actions.find((a) => a.id === actionId);\n      if (!action) return;\n\n      const isAnyActionExecuting = executingActionId !== null;\n      if (action.disabled || action.loading || isAnyActionExecuting) {\n        return;\n      }\n\n      if (action.confirmLabel && confirmingActionId !== action.id) {\n        setConfirmingActionId(action.id);\n        return;\n      }\n\n      if (onBeforeAction) {\n        const shouldProceed = await onBeforeAction(action.id);\n        if (!shouldProceed) {\n          setConfirmingActionId(null);\n          return;\n        }\n      }\n\n      try {\n        setExecutingActionId(action.id);\n        await onAction(action.id);\n      } finally {\n        setExecutingActionId(null);\n        setConfirmingActionId(null);\n      }\n    },\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\n  );\n\n  const resolvedActions = useMemo(\n    () =>\n      actions.map((action) => {\n        const isConfirming = confirmingActionId === action.id;\n        const isThisActionExecuting = executingActionId === action.id;\n        const isLoading = action.loading || isThisActionExecuting;\n        const isDisabled =\n          action.disabled || (executingActionId !== null && !isThisActionExecuting);\n        const currentLabel =\n          isConfirming && action.confirmLabel\n            ? action.confirmLabel\n            : action.label;\n\n        return {\n          ...action,\n          currentLabel,\n          isConfirming,\n          isExecuting: isThisActionExecuting,\n          isDisabled,\n          isLoading,\n        };\n      }),\n    [actions, confirmingActionId, executingActionId],\n  );\n\n  return {\n    actions: resolvedActions,\n    runAction,\n    confirmingActionId,\n    executingActionId,\n  };\n}\n\n\n/**\n * JSON primitive type that can be serialized.\n */\ntype JsonPrimitive = string | number | boolean | null;\n\n/**\n * Valid row value types for serializable DataTable data.\n *\n * Supports:\n * - Primitives: string, number, boolean, null\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\n *\n * For complex data (objects with href/label, etc.), use column format configs\n * instead of putting objects in row data.\n *\n * @example\n * ```ts\n * // 👍 Good: Use primitives and primitive arrays\n * const row = {\n *   name: \"Widget\",\n *   price: 29.99,\n *   tags: [\"electronics\", \"featured\"],\n *   metrics: [1.2, 3.4, 5.6]\n * }\n *\n * // 🚫 Bad: Don't put objects in row data\n * const row = {\n *   link: { href: \"/path\", label: \"Click\" }  // Use format: { kind: 'link' } instead\n * }\n * ```\n */\nexport type RowPrimitive = JsonPrimitive | JsonPrimitive[];\nexport type DataTableRowData = Record<string, RowPrimitive>;\nexport type RowData = Record<string, unknown>;\nexport type ColumnKey<T extends object> = Extract<keyof T, string>;\n\nexport type FormatFor<V> = V extends number\n  ? Extract<FormatConfig, { kind: \"number\" | \"currency\" | \"percent\" | \"delta\" }>\n  : V extends boolean\n  ? Extract<FormatConfig, { kind: \"boolean\" | \"status\" | \"badge\" }>\n  : V extends (string | number | boolean | null)[]\n  ? Extract<FormatConfig, { kind: \"array\" }>\n  : V extends string\n  ? Extract<\n    FormatConfig,\n    { kind: \"text\" | \"link\" | \"date\" | \"badge\" | \"status\" }\n  >\n  : Extract<FormatConfig, { kind: \"text\" }>;\n\n/**\n * Column definition for DataTable\n *\n * @remarks\n * **Important:** Columns are sortable by default (opt-out pattern).\n * Set `sortable: false` explicitly to disable sorting for specific columns.\n */\nexport interface Column<\n  T extends object = DataTableRowData,\n  K extends ColumnKey<T> = ColumnKey<T>,\n> {\n  /** Unique identifier that maps to a key in the row data */\n  key: K;\n  /** Display text for the column header */\n  label: string;\n  /** Abbreviated label for narrow viewports */\n  abbr?: string;\n  /** Whether column is sortable. Default: true (opt-out pattern) */\n  sortable?: boolean;\n  /** Text alignment for column cells */\n  align?: \"left\" | \"right\" | \"center\";\n  /** Optional fixed width (CSS value) */\n  width?: string;\n  /** Enable text truncation with ellipsis */\n  truncate?: boolean;\n  /** Mobile display priority (primary = always visible, secondary = expandable, tertiary = hidden) */\n  priority?: \"primary\" | \"secondary\" | \"tertiary\";\n  /** Completely hide column on mobile viewports */\n  hideOnMobile?: boolean;\n  /** Formatting configuration for cell values */\n  format?: FormatFor<T[K]>;\n}\n\n/**\n * Serializable props that can come from LLM tool calls or be JSON-serialized.\n *\n * These props contain only primitive values, arrays, and plain objects -\n * no functions, class instances, or other non-serializable values.\n *\n * @example\n * ```tsx\n * const serializableProps: DataTableSerializableProps = {\n *   columns: [...],\n *   data: [...],\n *   rowIdKey: \"id\",\n *   defaultSort: { by: \"price\", direction: \"desc\" }\n * }\n * ```\n */\nexport interface DataTableSerializableProps<T extends object = RowData> {\n  /**\n   * Unique identifier for this tool UI instance in the conversation.\n   *\n   * Used for:\n   * - Assistant referencing (\"the table above\")\n   * - Receipt generation (linking actions to their source)\n   * - Narration context\n   *\n   * Should be stable across re-renders, meaningful, and unique within the conversation.\n   *\n   * @example \"data-table-expenses-q3\", \"search-results-repos\"\n   */\n  id: ToolUIId;\n  /** Optional surface role metadata (serializable) */\n  role?: ToolUIRole;\n  /** Optional receipt metadata for consequential outcomes (serializable) */\n  receipt?: ToolUIReceipt;\n  /** Column definitions */\n  columns: Column<T>[];\n  /** Row data (primitives only - no functions or class instances) */\n  data: T[];\n  /**\n   * Layout mode for the component.\n   * - 'auto' (default): Container queries choose table/cards\n   * - 'table': Force table layout\n   * - 'cards': Force stacked card layout\n   */\n  layout?: \"auto\" | \"table\" | \"cards\";\n  /**\n   * Key in row data to use as unique identifier for React keys\n   *\n   * **Strongly recommended:** Always provide this for dynamic data to prevent\n   * reconciliation issues (focus traps, animation glitches, incorrect state preservation)\n   * when data reorders. Falls back to array index if omitted (only acceptable for static mock data).\n   *\n   * @example rowIdKey=\"id\" or rowIdKey=\"uuid\"\n   */\n  rowIdKey?: ColumnKey<T>;\n  /**\n   * Uncontrolled initial sort state (table manages its own sort state internally)\n   *\n   * **Sorting cycle:** Clicking column headers cycles through tri-state:\n   * 1. none (unsorted) → 2. asc → 3. desc → 4. none (back to unsorted)\n   *\n   * @example\n   * ```tsx\n   * // Start with descending price sort\n   * <DataTable defaultSort={{ by: \"price\", direction: \"desc\" }} />\n   * ```\n   */\n  defaultSort?: { by?: ColumnKey<T>; direction?: \"asc\" | \"desc\" };\n  /**\n   * Controlled sort state (use with onSortChange from client props)\n   *\n   * When provided, you must also provide `onSortChange` to handle sort updates.\n   * The table will cycle through: none → asc → desc → none.\n   *\n   * @example\n   * ```tsx\n   * const [sort, setSort] = useState({ by: \"price\", direction: \"desc\" })\n   * <DataTable sort={sort} onSortChange={setSort} />\n   * ```\n   */\n  sort?: { by?: ColumnKey<T>; direction?: \"asc\" | \"desc\" };\n  /** Empty state message */\n  emptyMessage?: string;\n  /** Max table height with vertical scroll (CSS value) */\n  maxHeight?: string;\n  /**\n   * BCP47 locale for formatting and sorting (e.g., 'en-US', 'de-DE', 'ja-JP')\n   *\n   * Defaults to 'en-US' to ensure consistent server/client rendering.\n   * Pass explicit locale for internationalization.\n   *\n   * @example\n   * ```tsx\n   * <DataTable locale=\"de-DE\" /> // German formatting\n   * <DataTable locale=\"ja-JP\" /> // Japanese formatting\n   * <DataTable />               // Uses 'en-US' default\n   * ```\n   */\n  locale?: string;\n}\n\n/**\n * Client-side React-only props that cannot be serialized.\n *\n * These props contain functions, component state, or other React-specific values\n * that must be provided by your React code (not from LLM tool calls).\n *\n * @example\n * ```tsx\n * const clientProps: DataTableClientProps = {\n *   isLoading: false,\n *   className: \"my-table\",\n *   onSortChange: (next) => setSort(next),\n *   responseActions: [{ id: \"export\", label: \"Export\" }],\n *   onResponseAction: (id) => console.log(id)\n * }\n * ```\n */\nexport interface DataTableClientProps<T extends object = RowData> {\n  /** Show loading skeleton */\n  isLoading?: boolean;\n  /** Additional CSS classes */\n  className?: string;\n  /**\n   * Sort change handler for controlled mode (required if sort is provided)\n   *\n   * **Tri-state cycle behavior:**\n   * - Click unsorted column: `{ by: \"column\", direction: \"asc\" }`\n   * - Click asc column: `{ by: \"column\", direction: \"desc\" }`\n   * - Click desc column: `{ by: \"column\", direction: undefined }` (returns to unsorted)\n   * - Click different column: `{ by: \"newColumn\", direction: \"asc\" }`\n   *\n   * @example\n   * ```tsx\n   * const [sort, setSort] = useState<{ by?: string; direction?: \"asc\" | \"desc\" }>({})\n   *\n   * <DataTable\n   *   sort={sort}\n   *   onSortChange={(next) => {\n   *     console.log(\"Sort changed:\", next)\n   *     setSort(next)\n   *   }}\n   * />\n   * ```\n   */\n  onSortChange?: (next: {\n    by?: ColumnKey<T>;\n    direction?: \"asc\" | \"desc\";\n  }) => void;\n  /** Optional response actions rendered below the table */\n  responseActions?: ActionsProp;\n  onResponseAction?: (actionId: string) => void | Promise<void>;\n  onBeforeResponseAction?: (actionId: string) => boolean | Promise<boolean>;\n}\n\n/**\n * Complete props for the DataTable component.\n *\n * Combines serializable props (can come from LLM tool calls) with client-side\n * React-only props. This separation makes the boundary explicit and prevents\n * accidental serialization of non-serializable values.\n *\n * @see {@link DataTableSerializableProps} for props that can be JSON-serialized\n * @see {@link DataTableClientProps} for React-only props\n * @see {@link parseSerializableDataTable} for parsing LLM tool call results\n *\n * @example\n * ```tsx\n * // From LLM tool call\n * const serializableProps = parseSerializableDataTable(llmResult)\n *\n * // Combine with React-specific props\n * <DataTable\n *   {...serializableProps}\n *   onSortChange={setSort}\n *   responseActions={[{ id: \"export\", label: \"Export\" }]}\n *   onResponseAction={(id) => handleAction(id)}\n *   isLoading={loading}\n * />\n * ```\n */\nexport interface DataTableProps<T extends object = RowData>\n  extends DataTableSerializableProps<T>, DataTableClientProps<T> { }\n\nexport interface DataTableContextValue<T extends object = RowData> {\n  columns: Column<T>[];\n  data: T[];\n  rowIdKey?: ColumnKey<T>;\n  sortBy?: ColumnKey<T>;\n  sortDirection?: \"asc\" | \"desc\";\n  toggleSort?: (key: ColumnKey<T>) => void;\n  id?: string;\n  isLoading?: boolean;\n  locale?: string;\n}\n\n/**\n * Sort an array of objects by a key\n */\nexport function sortData<T, K extends Extract<keyof T, string>>(\n  data: T[],\n  key: K,\n  direction: \"asc\" | \"desc\",\n  locale?: string,\n): T[] {\n  const get = (obj: T, k: K): unknown => (obj as Record<string, unknown>)[k];\n  const collator = new Intl.Collator(locale, {\n    numeric: true,\n    sensitivity: \"base\",\n  });\n  return [...data].sort((a, b) => {\n    const aVal = get(a, key);\n    const bVal = get(b, key);\n\n    // Handle nulls\n    if (aVal == null && bVal == null) return 0;\n    if (aVal == null) return 1;\n    if (bVal == null) return -1;\n\n    // Type-specific comparison\n    // Numbers\n    if (typeof aVal === \"number\" && typeof bVal === \"number\") {\n      return direction === \"asc\" ? aVal - bVal : bVal - aVal;\n    }\n    // Dates (Date instances)\n    if (aVal instanceof Date && bVal instanceof Date) {\n      const diff = aVal.getTime() - bVal.getTime();\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Booleans: false < true\n    if (typeof aVal === \"boolean\" && typeof bVal === \"boolean\") {\n      const diff = aVal === bVal ? 0 : aVal ? 1 : -1;\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Arrays: compare length\n    if (Array.isArray(aVal) && Array.isArray(bVal)) {\n      const diff = aVal.length - bVal.length;\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Strings that look like numbers -> numeric compare\n    if (typeof aVal === \"string\" && typeof bVal === \"string\") {\n      const numA = parseNumericLike(aVal);\n      const numB = parseNumericLike(bVal);\n      if (numA != null && numB != null) {\n        const diff = numA - numB;\n        return direction === \"asc\" ? diff : -diff;\n      }\n      // ISO-like date strings\n      if (/^\\d{4}-\\d{2}-\\d{2}/.test(aVal) && /^\\d{4}-\\d{2}-\\d{2}/.test(bVal)) {\n        const da = new Date(aVal).getTime();\n        const db = new Date(bVal).getTime();\n        const diff = da - db;\n        return direction === \"asc\" ? diff : -diff;\n      }\n    }\n\n    // Fallback: locale-aware string compare with numeric collation\n    const aStr = String(aVal);\n    const bStr = String(bVal);\n    const comparison = collator.compare(aStr, bStr);\n    return direction === \"asc\" ? comparison : -comparison;\n  });\n}\n\n/**\n * Return a human-friendly identifier for a row using common keys\n *\n * Accepts any JSON-serializable primitive or array of primitives.\n * Arrays are converted to comma-separated strings.\n */\nexport function getRowIdentifier(\n  row: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >,\n  identifierKey?: string,\n): string {\n  const candidate =\n    (identifierKey ? row[identifierKey] : undefined) ??\n    (row as Record<string, unknown>).name ??\n    (row as Record<string, unknown>).title ??\n    (row as Record<string, unknown>).id;\n\n  if (candidate == null) {\n    return \"\";\n  }\n\n  // Handle arrays by joining them\n  if (Array.isArray(candidate)) {\n    return candidate.map((v) => (v === null ? \"null\" : String(v))).join(\", \");\n  }\n\n  return String(candidate).trim();\n}\n\n/**\n * Parse a string that represents a numeric value, handling various formats:\n * - Currency symbols: $, €, £, ¥, etc.\n * - Percent symbols: %\n * - Accounting negatives: (1234) → -1234\n * - Thousands/decimal separators: 1,234.56 or 1.234,56\n * - Compact notation: 2.8T (trillion), 1.5M (million), 500K (thousand)\n * - Byte suffixes: 768B (bytes), 1.5KB, 2GB, 1TB\n *\n * Note: Single \"B\" is disambiguated - integers < 1024 are bytes, otherwise billions.\n *\n * @param input - String to parse\n * @returns Parsed number or null if unparseable\n *\n * @example\n * parseNumericLike(\"$1,234.56\") // 1234.56\n * parseNumericLike(\"2.8T\") // 2800000000000\n * parseNumericLike(\"768B\") // 768\n * parseNumericLike(\"50%\") // 50\n * parseNumericLike(\"(1234)\") // -1234\n */\nexport function parseNumericLike(input: string): number | null {\n  // Normalize whitespace (spaces, NBSPs, thin spaces)\n  let s = input.replace(/[\\u00A0\\u202F\\s]/g, \"\").trim();\n  if (!s) return null;\n\n  // Accounting negatives: (1234) -> -1234\n  s = s.replace(/^\\((.*)\\)$/g, \"-$1\");\n\n  // Strip common currency and percent symbols\n  s = s.replace(/[%$€£¥₩₹₽₺₪₫฿₦₴₡₲₵₸]/g, \"\");\n\n  const lastComma = s.lastIndexOf(\",\");\n  const lastDot = s.lastIndexOf(\".\");\n  if (lastComma !== -1 && lastDot !== -1) {\n    // Decide decimal by whichever occurs last\n    const decimalSep = lastComma > lastDot ? \",\" : \".\";\n    const thousandSep = decimalSep === \",\" ? \".\" : \",\";\n    s = s.split(thousandSep).join(\"\");\n    s = s.replace(decimalSep, \".\");\n  } else if (lastComma !== -1) {\n    // Only comma present\n    const frac = s.length - lastComma - 1;\n    if (frac === 2 || frac === 3) s = s.replace(/,/g, \".\");\n    else s = s.replace(/,/g, \"\");\n  } else if (lastDot !== -1) {\n    // Only dot present; if multiple dots, treat as thousands and strip\n    if ((s.match(/\\./g) || []).length > 1) s = s.replace(/\\./g, \"\");\n  }\n\n  // Handle compact notation (K, M, B, T, P, G) and byte suffixes (KB, MB, GB, TB, PB)\n  const compactMatch = s.match(/^([+-]?\\d+\\.?\\d*|\\d*\\.\\d+)([KMBTPG]B?|B)$/i);\n  if (compactMatch) {\n    const baseNum = Number(compactMatch[1]);\n    if (Number.isNaN(baseNum)) return null;\n\n    const suffix = compactMatch[2].toUpperCase();\n\n    // Disambiguate single \"B\" (bytes vs billions)\n    // If whole number < 1024, treat as bytes. Otherwise, billions.\n    if (suffix === \"B\") {\n      const isLikelyBytes = Number.isInteger(baseNum) && baseNum < 1024;\n      return isLikelyBytes ? baseNum : baseNum * 1e9;\n    }\n\n    const multipliers: Record<string, number> = {\n      K: 1e3,\n      KB: 1024, // Kilo: metric vs binary\n      M: 1e6,\n      MB: 1024 ** 2, // Mega\n      G: 1e9,\n      GB: 1024 ** 3, // Giga\n      T: 1e12,\n      TB: 1024 ** 4, // Tera\n      P: 1e15,\n      PB: 1024 ** 5, // Peta\n    };\n\n    return baseNum * (multipliers[suffix] ?? 1);\n  }\n\n  if (/^[+-]?(?:\\d+\\.?\\d*|\\d*\\.\\d+)$/.test(s)) {\n    const n = Number(s);\n    return Number.isNaN(n) ? null : n;\n  }\n  return null;\n}\n\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:scale-y-[-1]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"group data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-y-clip overflow-x-visible text-sm\"\n      {...props}\n    >\n      <div\n        className={cn(\n          \"pt-0 pb-4\",\n          \"group-data-[state=open]:animate-in group-data-[state=open]:fade-in-0\",\n          \"group-data-[state=closed]:animate-out group-data-[state=closed]:fade-out-0\",\n          \"duration-200\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n\n\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n        homeCTA: \"rounded-full px-6 py-3 text-lg w-fit text-start\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n\n\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableHead,\n  TableRow,\n  TableCell,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/data-table.tsx"
    }
  ],
  "type": "registry:ui"
}
