{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:block",
  "title": "Data Table",
  "description": "Sortable, responsive data tables for tool call results.",
  "dependencies": [
    "zod"
  ],
  "registryDependencies": [
    "accordion",
    "badge",
    "button",
    "dropdown-menu",
    "table",
    "tooltip"
  ],
  "files": [
    {
      "path": "components/tool-ui/data-table/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/data-table/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn           → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button       → shadcn/ui Button\n *   DropdownMenu → shadcn/ui DropdownMenu\n *   Accordion    → shadcn/ui Accordion\n *   Tooltip      → shadcn/ui Tooltip\n *   Badge        → shadcn/ui Badge\n *   Table        → shadcn/ui Table\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nexport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nexport { Badge } from \"@/components/ui/badge\";\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableHead,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\n"
    },
    {
      "path": "components/tool-ui/data-table/data-table.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/data-table/data-table.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  cn,\n  Table,\n  TableBody,\n  TableRow,\n  TableCell,\n  TableHeader,\n  TableHead,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"./_adapter\";\nimport {\n  sortData,\n  createDataTableRowKeys,\n  getDataTableMobileDescriptionId,\n} from \"./utilities\";\nimport { renderFormattedValue } from \"./formatters\";\nimport type {\n  DataTableProps,\n  DataTableContextValue,\n  RowData,\n  DataTableRowData,\n  ColumnKey,\n  Column,\n} from \"./types\";\nimport type { FormatConfig } from \"./formatters\";\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.use(DataTableContext) as\n    | DataTableContextValue<T>\n    | undefined;\n  if (!context) {\n    throw new Error(\"useDataTable must be used within <DataTable.Provider />\");\n  }\n  return context;\n}\n\ntype DataTableLayout = \"auto\" | \"table\" | \"cards\";\n\ntype DataTableBaseProps<T extends object = RowData> = DataTableProps<T> & {\n  layout: DataTableLayout;\n};\n\ntype DataTableProviderProps<T extends object = RowData> = Pick<\n  DataTableProps<T>,\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"onSortChange\"\n  | \"id\"\n  | \"locale\"\n> & {\n  children: React.ReactNode;\n};\n\nfunction DataTableProvider<T extends object = RowData>({\n  columns,\n  data: rawData,\n  rowIdKey,\n  defaultSort,\n  sort: controlledSort,\n  id,\n  onSortChange,\n  locale,\n  children,\n}: DataTableProviderProps<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    locale: resolvedLocale,\n  };\n\n  return (\n    <DataTableContext.Provider value={contextValue}>\n      {children}\n    </DataTableContext.Provider>\n  );\n}\n\ninterface DataTableLayoutProps {\n  layout: DataTableLayout;\n  emptyMessage: string;\n  maxHeight?: string;\n  className?: string;\n}\n\nfunction DataTableLayout({\n  layout,\n  emptyMessage,\n  maxHeight,\n  className,\n}: DataTableLayoutProps) {\n  const { columns, data, rowIdKey, sortBy, sortDirection, id } = useDataTable();\n  const rowKeys = React.useMemo(\n    () =>\n      createDataTableRowKeys(\n        data as Array<Record<string, unknown>>,\n        rowIdKey ? String(rowIdKey) : undefined,\n      ),\n    [data, rowIdKey],\n  );\n  const mobileDescriptionId = React.useMemo(\n    () => getDataTableMobileDescriptionId(String(id ?? \"data-table\")),\n    [id],\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  return (\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            <Table>\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              {data.length === 0 ? (\n                <DataTableEmpty message={emptyMessage} />\n              ) : (\n                <DataTableContent />\n              )}\n            </Table>\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={mobileDescriptionId}\n      >\n        <div id={mobileDescriptionId} 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        {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 rowKey = rowKeys[i];\n              return (\n                <DataTableAccordionCard\n                  key={rowKey}\n                  row={row as unknown as DataTableRowData}\n                  index={i}\n                  rowKey={rowKey}\n                  isFirst={i === 0}\n                />\n              );\n            })}\n          </div>\n        )}\n      </div>\n\n      {sortAnnouncement && (\n        <div className=\"sr-only\" aria-live=\"polite\">\n          {sortAnnouncement}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction DataTableBase<T extends object = RowData>(\n  props: DataTableBaseProps<T>,\n) {\n  const {\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    onSortChange,\n    id,\n    locale,\n    layout,\n    emptyMessage = \"No data available\",\n    maxHeight,\n    className,\n  } = props;\n\n  return (\n    <DataTableProvider\n      columns={columns}\n      data={data}\n      rowIdKey={rowIdKey}\n      defaultSort={defaultSort}\n      sort={sort}\n      onSortChange={onSortChange}\n      id={id}\n      locale={locale}\n    >\n      <DataTableLayout\n        layout={layout}\n        emptyMessage={emptyMessage}\n        maxHeight={maxHeight}\n        className={className}\n      />\n    </DataTableProvider>\n  );\n}\n\nfunction DataTableRoot<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"auto\" />;\n}\n\nfunction DataTableTable<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"table\" />;\n}\n\nfunction DataTableCards<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"cards\" />;\n}\n\ntype DataTableComponent = {\n  <T extends object = RowData>(props: DataTableProps<T>): React.ReactElement;\n  Table: typeof DataTableTable;\n  Cards: typeof DataTableCards;\n  Provider: typeof DataTableProvider;\n};\n\nexport const DataTable = Object.assign(DataTableRoot, {\n  Table: DataTableTable,\n  Cards: DataTableCards,\n  Provider: DataTableProvider,\n}) as DataTableComponent;\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 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 } = 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 = !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 rowKeys = React.useMemo(\n    () =>\n      createDataTableRowKeys(\n        data as Array<Record<string, unknown>>,\n        rowIdKey ? String(rowIdKey) : undefined,\n      ),\n    [data, rowIdKey],\n  );\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. Falling back to inferred/content-derived row keys. \" +\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 rowKey = rowKeys[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  rowKey: string;\n  isFirst?: boolean;\n}\n\nfunction getDataTableRowDomId(rowKey: string): string {\n  return encodeURIComponent(rowKey).replace(/%/g, \"_\");\n}\n\nfunction DataTableAccordionCard({\n  row,\n  index,\n  rowKey,\n  isFirst = false,\n}: DataTableAccordionCardProps) {\n  const { columns, locale } = useDataTable();\n\n  const { primary, secondary } = React.useMemo(\n    () => categorizeColumns(columns),\n    [columns],\n  );\n\n  if (secondary.length === 0) {\n    return (\n      <SimpleCard\n        row={row}\n        columns={primary}\n        index={index}\n        rowKey={rowKey}\n        isFirst={isFirst}\n      />\n    );\n  }\n\n  const primaryColumn = primary[0];\n  const remainingPrimaryColumns = primary.slice(1);\n\n  const stableRowId = getDataTableRowDomId(rowKey);\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  rowKey,\n  isFirst = false,\n}: {\n  row: DataTableRowData;\n  columns: Column[];\n  index: number;\n  rowKey: string;\n  isFirst?: boolean;\n}) {\n  const { locale } = useDataTable();\n  const primaryColumn = columns[0];\n  const otherColumns = columns.slice(1);\n\n  const stableRowId = getDataTableRowDomId(rowKey);\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"
    },
    {
      "path": "components/tool-ui/data-table/formatters.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/data-table/formatters.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from \"./_adapter\";\nimport { resolveSafeNavigationHref } from \"../shared/media\";\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  locale?: string;\n}\n\nexport function DeltaValue({ value, options, locale }: 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 absValue = Math.abs(value);\n  const formatted = new Intl.NumberFormat(locale, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  }).format(absValue);\n\n  const display =\n    showSign && !isNeutral\n      ? isNegative\n        ? `-${formatted}`\n        : `+${formatted}`\n      : 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  locale?: string;\n}\n\nexport function PercentValue({ value, options, locale }: 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 : value / 100;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    style: \"percent\",\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n    signDisplay: showSign ? \"always\" : \"auto\",\n  }).format(numeric);\n\n  return <span className=\"tabular-nums\">{formatted}</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 className=\"text-muted-foreground\">{value}</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.trunc((date.getTime() - now.getTime()) / 1000);\n  const absDiffInSeconds = Math.abs(diffInSeconds);\n\n  if (absDiffInSeconds < 60) return \"just now\";\n\n  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n\n  if (absDiffInSeconds < 3600) {\n    const mins = Math.trunc(diffInSeconds / 60);\n    return rtf.format(mins, \"minute\");\n  }\n  if (absDiffInSeconds < 86400) {\n    const hours = Math.trunc(diffInSeconds / 3600);\n    return rtf.format(hours, \"hour\");\n  }\n  if (absDiffInSeconds < 604800) {\n    const days = Math.trunc(diffInSeconds / 86400);\n    return rtf.format(days, \"day\");\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 rawHref =\n    options?.hrefKey && row ? String(row[options.hrefKey] ?? \"\") : value;\n  const href = resolveSafeNavigationHref(rawHref);\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} locale={locale} />;\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 (\n        <PercentValue value={Number(value)} options={fmt} locale={locale} />\n      );\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"
    },
    {
      "path": "components/tool-ui/data-table/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/data-table/index.tsx",
      "content": "export { DataTable, useDataTable } from \"./data-table\";\n\nexport { renderFormattedValue } from \"./formatters\";\nexport {\n  NumberValue,\n  CurrencyValue,\n  PercentValue,\n  DeltaValue,\n  DateValue,\n  BooleanValue,\n  LinkValue,\n  BadgeValue,\n  StatusBadge,\n  ArrayValue,\n} from \"./formatters\";\n\nexport type {\n  Column,\n  DataTableProps,\n  DataTableSerializableProps,\n  DataTableClientProps,\n  DataTableRowData,\n  RowPrimitive,\n  RowData,\n  ColumnKey,\n} from \"./types\";\nexport type { FormatConfig } from \"./formatters\";\n\nexport { sortData, parseNumericLike } from \"./utilities\";\n"
    },
    {
      "path": "components/tool-ui/data-table/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/data-table/README.md",
      "content": "# Data Table\n\nImplementation for the \"data-table\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/data-table/index.tsx\n- serializable schema + parse helpers: components/tool-ui/data-table/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/data-table/content.mdx\n- Preset payload: lib/presets/data-table.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/data-table/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/data-table/schema.ts",
      "content": "import { z } from \"zod\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport type { Column, DataTableProps, RowData } from \"./types\";\n\nconst AlignEnum = z.enum([\"left\", \"right\", \"center\"]);\nconst PriorityEnum = z.enum([\"primary\", \"secondary\", \"tertiary\"]);\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([\n  z.string(),\n  z.number(),\n  z.boolean(),\n  z.null(),\n]);\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 * - optional presentation props: rowIdKey, sort/defaultSort, locale, etc.\n *\n * Non-serializable props like `onSortChange`, `className`, and sibling action surfaces\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  rowIdKey: z.string().optional(),\n  defaultSort: z\n    .object({\n      by: z.string().optional(),\n      direction: z.enum([\"asc\", \"desc\"]).optional(),\n    })\n    .optional(),\n  sort: z\n    .object({\n      by: z.string().optional(),\n      direction: z.enum([\"asc\", \"desc\"]).optional(),\n    })\n    .optional(),\n  emptyMessage: z.string().optional(),\n  maxHeight: z.string().optional(),\n  locale: z.string().optional(),\n});\n\nconst SerializableDataTableSchemaContract = defineToolUiContract(\n  \"DataTable\",\n  SerializableDataTableSchema,\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`)\n * - React-specific props (`className`)\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, className).\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 *     />\n *   )\n * }\n * ```\n */\nexport function parseSerializableDataTable(\n  input: unknown,\n): Pick<\n  DataTableProps<RowData>,\n  | \"id\"\n  | \"role\"\n  | \"receipt\"\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"emptyMessage\"\n  | \"maxHeight\"\n  | \"locale\"\n> {\n  const {\n    id,\n    role,\n    receipt,\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    emptyMessage,\n    maxHeight,\n    locale,\n  } = SerializableDataTableSchemaContract.parse(input);\n  return {\n    id,\n    role,\n    receipt,\n    columns: columns as unknown as Column<RowData>[],\n    data: data as RowData[],\n    rowIdKey: rowIdKey as keyof RowData | undefined,\n    defaultSort: defaultSort\n      ? {\n          by: defaultSort.by as keyof RowData | undefined,\n          direction: defaultSort.direction,\n        }\n      : undefined,\n    sort: sort\n      ? {\n          by: sort.by as keyof RowData | undefined,\n          direction: sort.direction,\n        }\n      : undefined,\n    emptyMessage,\n    maxHeight,\n    locale,\n  };\n}\n\nexport function safeParseSerializableDataTable(\n  input: unknown,\n): Pick<\n  DataTableProps<RowData>,\n  | \"id\"\n  | \"role\"\n  | \"receipt\"\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"emptyMessage\"\n  | \"maxHeight\"\n  | \"locale\"\n> | null {\n  const res = SerializableDataTableSchemaContract.safeParse(input);\n  if (!res) return null;\n  const {\n    id,\n    role,\n    receipt,\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    emptyMessage,\n    maxHeight,\n    locale,\n  } = res;\n  return {\n    id,\n    role,\n    receipt,\n    columns: columns as unknown as Column<RowData>[],\n    data: data as RowData[],\n    rowIdKey: rowIdKey as keyof RowData | undefined,\n    defaultSort: defaultSort\n      ? {\n          by: defaultSort.by as keyof RowData | undefined,\n          direction: defaultSort.direction,\n        }\n      : undefined,\n    sort: sort\n      ? {\n          by: sort.by as keyof RowData | undefined,\n          direction: sort.direction,\n        }\n      : undefined,\n    emptyMessage,\n    maxHeight,\n    locale,\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/data-table/types.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/data-table/types.ts",
      "content": "import type { ToolUIId, ToolUIReceipt, ToolUIRole } from \"../shared/schema\";\nimport type { FormatConfig } from \"./formatters\";\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   * 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 *   className: \"my-table\",\n *   onSortChange: (next) => setSort(next),\n *   // Compose local/decision actions externally via LocalActions/DecisionActions\n * }\n * ```\n */\nexport interface DataTableClientProps<T extends object = RowData> {\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}\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 *   // Render sibling LocalActions / DecisionActions where needed\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  locale?: string;\n}\n"
    },
    {
      "path": "components/tool-ui/data-table/utilities.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/data-table/utilities.ts",
      "content": "/**\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\nfunction stableStringify(value: unknown): string {\n  if (value == null) return \"null\";\n  if (typeof value === \"string\") return JSON.stringify(value);\n  if (\n    typeof value === \"number\" ||\n    typeof value === \"boolean\" ||\n    typeof value === \"bigint\"\n  ) {\n    return String(value);\n  }\n  if (Array.isArray(value)) {\n    return `[${value.map((item) => stableStringify(item)).join(\",\")}]`;\n  }\n  if (typeof value === \"object\") {\n    const entries = Object.entries(value as Record<string, unknown>).sort(\n      ([a], [b]) => a.localeCompare(b),\n    );\n    return `{${entries\n      .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)\n      .join(\",\")}}`;\n  }\n  return JSON.stringify(String(value));\n}\n\nfunction hashString(value: string): string {\n  let hash = 5381;\n  for (let i = 0; i < value.length; i++) {\n    hash = (hash * 33) ^ value.charCodeAt(i);\n  }\n  return (hash >>> 0).toString(36);\n}\n\n/**\n * Create deterministic, reorder-stable React keys for DataTable rows.\n *\n * - Uses `identifierKey` or common identifier fields as the primary base.\n * - Falls back to stable content fingerprints when no identifier exists.\n * - Disambiguates duplicates without relying on array index.\n */\nexport function createDataTableRowKeys(\n  rows: Array<Record<string, unknown>>,\n  identifierKey?: string,\n): string[] {\n  const canonicalRows = rows.map((row) => stableStringify(row));\n\n  const baseKeys = rows.map((row, index) => {\n    const identifier = getRowIdentifier(\n      row as Record<\n        string,\n        string | number | boolean | null | (string | number | boolean | null)[]\n      >,\n      identifierKey,\n    );\n\n    if (identifier) {\n      return `id:${identifier}`;\n    }\n\n    return `row:${hashString(canonicalRows[index])}`;\n  });\n\n  const baseCounts = new Map<string, number>();\n  baseKeys.forEach((key) => {\n    baseCounts.set(key, (baseCounts.get(key) ?? 0) + 1);\n  });\n\n  const usedKeys = new Map<string, number>();\n\n  return rows.map((row, index) => {\n    const baseKey = baseKeys[index];\n    if ((baseCounts.get(baseKey) ?? 0) === 1) {\n      return baseKey;\n    }\n\n    const rowFingerprint = hashString(canonicalRows[index]);\n    let disambiguatedKey = `${baseKey}::${rowFingerprint}`;\n\n    const seenCount = usedKeys.get(disambiguatedKey) ?? 0;\n    usedKeys.set(disambiguatedKey, seenCount + 1);\n    if (seenCount > 0) {\n      disambiguatedKey = `${disambiguatedKey}::d${seenCount + 1}`;\n    }\n\n    return disambiguatedKey;\n  });\n}\n\nfunction sanitizeDomIdToken(value: string): string {\n  return encodeURIComponent(value).replace(/%/g, \"_\");\n}\n\nexport function getDataTableMobileDescriptionId(surfaceId: string): string {\n  return `${sanitizeDomIdToken(surfaceId)}-mobile-table-description`;\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  function hasGroupedThousands(value: string, sep: \",\" | \".\"): boolean {\n    const unsigned = value.replace(/^[+-]/, \"\");\n    const parts = unsigned.split(sep);\n    if (parts.length < 2) return false;\n    if (parts.some((part) => part.length === 0)) return false;\n    if (!/^\\d{1,3}$/.test(parts[0])) return false;\n    if (parts[0] === \"0\") return false;\n    return parts.slice(1).every((part) => /^\\d{3}$/.test(part));\n  }\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    if (hasGroupedThousands(s, \",\")) {\n      s = s.replace(/,/g, \"\");\n    } else {\n      const frac = s.length - lastComma - 1;\n      if (frac >= 1 && frac <= 3) s = s.replace(/,/g, \".\");\n      else s = s.replace(/,/g, \"\");\n    }\n  } else if (lastDot !== -1) {\n    // Only dot present; normalize grouped thousands separators.\n    if (hasGroupedThousands(s, \".\")) {\n      s = s.replace(/\\./g, \"\");\n    } else if ((s.match(/\\./g) || []).length > 1) {\n      s = s.replace(/\\./g, \"\");\n    }\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"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/media/aspect-ratio.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/aspect-ratio.ts",
      "content": "import { z } from \"zod\";\n\nexport const AspectRatioSchema = z\n  .enum([\"auto\", \"1:1\", \"4:3\", \"16:9\", \"9:16\"])\n  .default(\"auto\");\n\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\n\nexport const MediaFitSchema = z.enum([\"cover\", \"contain\"]).default(\"cover\");\n\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\n\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\n  auto: \"\",\n  \"1:1\": \"aspect-square\",\n  \"4:3\": \"aspect-[4/3]\",\n  \"16:9\": \"aspect-video\",\n  \"9:16\": \"aspect-[9/16]\",\n};\n\nexport function getRatioClass(ratio: AspectRatio): string {\n  return RATIO_CLASS_MAP[ratio];\n}\n\nexport function getFitClass(fit: MediaFit): string {\n  return fit === \"cover\" ? \"object-cover\" : \"object-contain\";\n}\n"
    },
    {
      "path": "components/tool-ui/shared/media/format-utils.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/format-utils.ts",
      "content": "/**\n * Format duration in milliseconds to human-readable string.\n * @example formatDuration(128000) => \"2:08\"\n * @example formatDuration(3661000) => \"1:01:01\"\n */\nexport function formatDuration(durationMs: number): string {\n  const totalSeconds = Math.round(durationMs / 1000);\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n\n  if (hours > 0) {\n    return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds\n      .toString()\n      .padStart(2, \"0\")}`;\n  }\n  return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n}\n\n/**\n * Format file size in bytes to human-readable string.\n * @example formatFileSize(1024) => \"1 KB\"\n * @example formatFileSize(1536000) => \"1.5 MB\"\n */\nexport function formatFileSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\"];\n  let size = bytes / 1024;\n  let unit = 0;\n  while (size >= 1024 && unit < units.length - 1) {\n    size /= 1024;\n    unit += 1;\n  }\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/media/index.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/index.ts",
      "content": "export {\n  AspectRatioSchema,\n  MediaFitSchema,\n  RATIO_CLASS_MAP,\n  getRatioClass,\n  getFitClass,\n  type AspectRatio,\n  type MediaFit,\n} from \"./aspect-ratio\";\n\nexport { OVERLAY_GRADIENT } from \"./overlay-gradient\";\n\nexport { formatDuration, formatFileSize } from \"./format-utils\";\n\nexport { sanitizeHref } from \"./sanitize-href\";\nexport {\n  resolveSafeNavigationHref,\n  openSafeNavigationHref,\n} from \"./safe-navigation\";\n"
    },
    {
      "path": "components/tool-ui/shared/media/overlay-gradient.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/overlay-gradient.ts",
      "content": "/**\n * Eased gradient for hover overlays on media elements.\n * Creates a smooth fade from opaque black at top to transparent.\n *\n * @see https://larsenwork.com/easing-gradients/\n */\nexport const OVERLAY_GRADIENT = `linear-gradient(\n  to bottom,\n  hsl(0, 0%, 0%) 0%,\n  hsla(0, 0%, 0%, 0.987) 8.3%,\n  hsla(0, 0%, 0%, 0.951) 16.6%,\n  hsla(0, 0%, 0%, 0.896) 24.6%,\n  hsla(0, 0%, 0%, 0.825) 32.5%,\n  hsla(0, 0%, 0%, 0.741) 40.1%,\n  hsla(0, 0%, 0%, 0.648) 47.6%,\n  hsla(0, 0%, 0%, 0.55) 54.8%,\n  hsla(0, 0%, 0%, 0.45) 61.7%,\n  hsla(0, 0%, 0%, 0.352) 68.3%,\n  hsla(0, 0%, 0%, 0.259) 74.5%,\n  hsla(0, 0%, 0%, 0.175) 80.4%,\n  hsla(0, 0%, 0%, 0.104) 86%,\n  hsla(0, 0%, 0%, 0.049) 91.1%,\n  hsla(0, 0%, 0%, 0.013) 95.8%,\n  hsla(0, 0%, 0%, 0) 100%\n)` as const;\n"
    },
    {
      "path": "components/tool-ui/shared/media/safe-navigation.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/safe-navigation.ts",
      "content": "import { sanitizeHref } from \"./sanitize-href\";\n\nexport function resolveSafeNavigationHref(\n  ...candidates: Array<string | null | undefined>\n): string | undefined {\n  for (const candidate of candidates) {\n    const safeHref = sanitizeHref(candidate ?? undefined);\n    if (safeHref) {\n      return safeHref;\n    }\n  }\n\n  return undefined;\n}\n\nexport function openSafeNavigationHref(href: string | undefined): boolean {\n  if (!href || typeof window === \"undefined\") {\n    return false;\n  }\n\n  window.open(href, \"_blank\", \"noopener,noreferrer\");\n  return true;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/media/sanitize-href.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/media/sanitize-href.ts",
      "content": "/**\n * Sanitize a URL to ensure it's safe for use in href attributes.\n * Allows:\n * - Absolute http(s) URLs\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\n *\n * @returns The sanitized URL string, or undefined if invalid/unsafe\n */\nexport function sanitizeHref(href?: string): string | undefined {\n  if (!href) return undefined;\n  const candidate = href.trim();\n  if (!candidate) return undefined;\n\n  if (\n    candidate.startsWith(\"/\") ||\n    candidate.startsWith(\"./\") ||\n    candidate.startsWith(\"../\") ||\n    candidate.startsWith(\"?\") ||\n    candidate.startsWith(\"#\")\n  ) {\n    if (candidate.startsWith(\"//\")) return undefined;\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\n    if (/[\\u0000-\\u001F\\u007F]/.test(candidate)) return undefined;\n    return candidate;\n  }\n\n  try {\n    const url = new URL(candidate);\n    if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n      return url.toString();\n    }\n  } catch {\n    return undefined;\n  }\n  return undefined;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\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/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/schema.ts",
      "content": "import { z } from \"zod\";\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)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\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>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\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"
    }
  ]
}
