{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comparison-table",
  "title": "Comparison Table",
  "description": "Compare plans or products with sticky labels and highlighted differences.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "checkbox",
    "label",
    "table",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/comparison-table.tsx",
      "content": "\"use client\";\n\nimport { Check, Minus } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const COMPARISON_TABLE_VARIANTS = [\"bordered\", \"plain\", \"compact\"] as const;\nexport type ComparisonTableVariant = (typeof COMPARISON_TABLE_VARIANTS)[number];\nexport type ComparisonTableProps = ComponentProps<\"div\"> & {\n  variant?: ComparisonTableVariant;\n  highlightDifferences?: boolean;\n  defaultHighlightDifferences?: boolean;\n  onHighlightDifferencesChange?: (highlight: boolean) => void;\n};\n\ntype TableContext = {\n  id: string;\n  variant: ComparisonTableVariant;\n  highlight: boolean;\n  setHighlight: (highlight: boolean) => void;\n};\nconst Context = createContext<TableContext | null>(null);\nfunction useTable(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within ComparisonTable`);\n  return context;\n}\nconst RowContext = createContext<{ different: boolean } | null>(null);\nfunction useRow(part: string) {\n  const context = useContext(RowContext);\n  if (!context) throw new Error(`${part} must be used within ComparisonTableRow`);\n  return context;\n}\n\nconst marked =\n  \"bg-[color-mix(in_oklab,var(--primary)_9%,var(--card))] hover:bg-[color-mix(in_oklab,var(--primary)_9%,var(--card))]\";\n// Rows tint on hover; the sticky row header follows so the tint spans the full row.\nconst rowHover = \"hover:bg-[color-mix(in_oklab,var(--muted)_55%,var(--card))]\";\nconst headerHover = \"bg-card [tr:hover>&]:bg-[color-mix(in_oklab,var(--muted)_55%,var(--card))]\";\n// Hairlines between rows only; the last row sits on the container edge.\nconst cellRule = \"border-b [tr:last-child>&]:border-b-0\";\nconst badge = \"bg-primary/16 text-[11px]/4 font-medium text-primary-ink\";\n// Sticky headers cast a soft edge once the table scrolls under them.\nconst stickyShadow =\n  \"group-data-scrolled/comparison-table:shadow-[4px_0_8px_-4px_oklch(0_0_0/0.16)]\";\n// Uai cells size from padding and wrap; the shadcn cells are fixed-height and nowrap.\nconst cellReset = \"h-auto whitespace-normal\";\n\nfunction cellPadding(variant: ComparisonTableVariant) {\n  return variant === \"compact\" ? \"px-3 py-1.75\" : \"px-4 py-2.75\";\n}\n\nexport function ComparisonTable({\n  variant = \"bordered\",\n  highlightDifferences,\n  defaultHighlightDifferences = false,\n  onHighlightDifferencesChange,\n  className,\n  children,\n  ...props\n}: ComparisonTableProps) {\n  const id = useId();\n  const [internal, setInternal] = useState(defaultHighlightDifferences);\n  const highlight = highlightDifferences ?? internal;\n  const setHighlight = (next: boolean) => {\n    if (highlightDifferences === undefined) setInternal(next);\n    onHighlightDifferencesChange?.(next);\n  };\n  return (\n    <Context.Provider value={{ id, variant, highlight, setHighlight }}>\n      <div\n        data-slot=\"comparison-table\"\n        data-variant={variant}\n        data-highlight-differences={highlight || undefined}\n        className={cn(\"grid min-w-0 gap-3 text-[13px]/[18px] text-foreground\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\n\nexport function ComparisonTableHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"comparison-table-header\"\n      className={cn(\"flex flex-wrap items-center justify-between gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ComparisonTableTitle({ className, ...props }: ComponentProps<\"h3\">) {\n  const context = useTable(\"ComparisonTableTitle\");\n  return (\n    <h3\n      data-slot=\"comparison-table-title\"\n      className={cn(\"m-0 text-sm/5 font-semibold tracking-[-0.01em]\", className)}\n      {...props}\n      id={`${context.id}-title`}\n    />\n  );\n}\n\nexport function ComparisonTableDifferencesToggle({\n  children = \"Highlight differences\",\n  className,\n  id,\n  ...props\n}: Omit<\n  ComponentProps<typeof Checkbox>,\n  \"checked\" | \"defaultChecked\" | \"onCheckedChange\" | \"asChild\"\n>) {\n  const context = useTable(\"ComparisonTableDifferencesToggle\");\n  const checkboxId = id ?? `${context.id}-differences`;\n  return (\n    <Label\n      htmlFor={checkboxId}\n      data-slot=\"comparison-table-differences-toggle\"\n      className={cn(\n        \"inline-flex min-h-7 cursor-pointer items-center gap-2 text-[12.5px] leading-[18px] font-medium text-muted-foreground select-auto\",\n        className,\n      )}\n    >\n      <Checkbox\n        {...props}\n        id={checkboxId}\n        checked={context.highlight}\n        onCheckedChange={(checked) => context.setHighlight(checked === true)}\n        className=\"m-0 size-3.5 border-subtle-foreground data-[state=checked]:border-primary data-checked:border-primary shadow-none [&_svg]:size-3\"\n      />\n      {children}\n    </Label>\n  );\n}\n\nexport function ComparisonTableContent({\n  className,\n  children,\n  onScroll,\n  ...props\n}: Omit<ComponentProps<\"section\">, \"children\"> & { children: ReactNode }) {\n  const context = useTable(\"ComparisonTableContent\");\n  const compact = context.variant === \"compact\";\n  return (\n    <section\n      aria-labelledby={`${context.id}-title`}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: horizontally scrollable regions must be keyboard reachable.\n      tabIndex={0}\n      data-slot=\"comparison-table-content\"\n      className={cn(\n        // The focusable section scrolls, so the shadcn table container must not become a second scroller.\n        \"group/comparison-table max-w-full overflow-x-auto bg-card outline-offset-2 *:data-[slot=table-container]:overflow-visible\",\n        context.variant === \"plain\" ? \"border-0\" : \"border\",\n        compact ? \"rounded-xl\" : \"rounded-[14px]\",\n        className,\n      )}\n      {...props}\n      onScroll={(event) => {\n        onScroll?.(event);\n        event.currentTarget.toggleAttribute(\"data-scrolled\", event.currentTarget.scrollLeft > 0);\n      }}\n    >\n      <Table\n        aria-labelledby={`${context.id}-title`}\n        className={cn(\n          \"w-full min-w-[520px] border-separate border-spacing-0 tabular-nums\",\n          compact ? \"text-[12.5px]/[18px]\" : \"text-[13px]/[18px]\",\n        )}\n      >\n        {children}\n      </Table>\n    </section>\n  );\n}\n\nexport function ComparisonTableHead({ children, ...props }: ComponentProps<\"thead\">) {\n  useTable(\"ComparisonTableHead\");\n  return (\n    <TableHeader data-slot=\"comparison-table-head\" {...props}>\n      <TableRow className=\"border-0 hover:bg-transparent\">{children}</TableRow>\n    </TableHeader>\n  );\n}\n\nexport function ComparisonTableCorner({ className, ...props }: ComponentProps<\"th\">) {\n  const context = useTable(\"ComparisonTableCorner\");\n  return (\n    <TableHead\n      scope=\"col\"\n      data-slot=\"comparison-table-corner\"\n      className={cn(\n        \"sticky top-0 left-0 z-2 border-b bg-card text-left align-bottom text-[12px] font-medium text-subtle-foreground transition-shadow duration-150 ease-out motion-reduce:transition-none\",\n        stickyShadow,\n        cellReset,\n        cellPadding(context.variant),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ComparisonTableColumn({\n  recommended = false,\n  recommendedLabel = \"Recommended\",\n  children,\n  className,\n  ...props\n}: ComponentProps<\"th\"> & { recommended?: boolean; recommendedLabel?: string }) {\n  const context = useTable(\"ComparisonTableColumn\");\n  return (\n    <TableHead\n      scope=\"col\"\n      data-slot=\"comparison-table-column\"\n      data-recommended={recommended || undefined}\n      className={cn(\n        \"sticky top-0 z-1 border-b text-left align-bottom font-medium\",\n        cellReset,\n        cellPadding(context.variant),\n        context.variant === \"compact\" ? \"text-[12.5px]\" : \"text-[13px]\",\n        recommended\n          ? \"bg-[color-mix(in_oklab,var(--primary)_7%,var(--card))] shadow-[inset_0_2px_0_var(--primary)]\"\n          : \"bg-card\",\n        className,\n      )}\n      {...props}\n    >\n      {recommended ? (\n        <span className={cn(\"mb-1.5 block w-fit rounded-full px-2 py-px\", badge)}>\n          {recommendedLabel}\n        </span>\n      ) : null}\n      {children}\n    </TableHead>\n  );\n}\n\nexport function ComparisonTableBody(props: ComponentProps<\"tbody\">) {\n  useTable(\"ComparisonTableBody\");\n  return <TableBody data-slot=\"comparison-table-body\" {...props} />;\n}\n\nexport function ComparisonTableRow({\n  different = false,\n  className,\n  ...props\n}: ComponentProps<\"tr\"> & { different?: boolean }) {\n  const context = useTable(\"ComparisonTableRow\");\n  const isMarked = context.highlight && different;\n  return (\n    <RowContext.Provider value={{ different }}>\n      <TableRow\n        data-slot=\"comparison-table-row\"\n        data-different={different || undefined}\n        className={cn(\n          \"border-0 [transition:background-color_120ms_ease-out,color_120ms_ease-out] motion-reduce:transition-none\",\n          isMarked ? marked : rowHover,\n          context.highlight && !different && \"text-subtle-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    </RowContext.Provider>\n  );\n}\n\nexport function ComparisonTableRowHeader({ children, className, ...props }: ComponentProps<\"th\">) {\n  const context = useTable(\"ComparisonTableRowHeader\");\n  const row = useRow(\"ComparisonTableRowHeader\");\n  const isMarked = context.highlight && row.different;\n  return (\n    <TableHead\n      scope=\"row\"\n      data-slot=\"comparison-table-row-header\"\n      className={cn(\n        \"sticky left-0 z-1 min-w-[140px] text-left font-medium [transition:background-color_120ms_ease-out,box-shadow_150ms_ease-out] motion-reduce:transition-none\",\n        cellReset,\n        cellPadding(context.variant),\n        cellRule,\n        isMarked\n          ? cn(\n              marked,\n              \"shadow-[inset_2px_0_0_var(--primary)]\",\n              \"group-data-scrolled/comparison-table:shadow-[inset_2px_0_0_var(--primary),4px_0_8px_-4px_oklch(0_0_0/0.16)]\",\n            )\n          : cn(headerHover, stickyShadow),\n        // Unchanged rows dim as a whole, so the row header inherits the row color.\n        context.highlight && !row.different ? \"text-inherit\" : \"text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {isMarked ? (\n        <span className={cn(\"ml-2 inline-block rounded-md px-1.5 align-[1px]\", badge)}>\n          Differs\n        </span>\n      ) : null}\n    </TableHead>\n  );\n}\n\nexport function ComparisonTableCell({ className, ...props }: ComponentProps<\"td\">) {\n  const context = useTable(\"ComparisonTableCell\");\n  return (\n    <TableCell\n      data-slot=\"comparison-table-cell\"\n      className={cn(\n        \"align-top font-medium\",\n        cellReset,\n        cellPadding(context.variant),\n        cellRule,\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ComparisonTableCheck({\n  value,\n  includedLabel = \"Included\",\n  excludedLabel = \"Not included\",\n  className,\n  ...props\n}: ComponentProps<\"span\"> & { value: boolean; includedLabel?: string; excludedLabel?: string }) {\n  const Icon = value ? Check : Minus;\n  return (\n    <span\n      data-slot=\"comparison-table-check\"\n      data-value={value}\n      className={cn(\n        \"inline-grid size-5 place-items-center rounded-full\",\n        value ? \"bg-success/14 text-success-ink\" : \"text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <Icon size={value ? 13 : 14} strokeWidth={value ? 2.25 : 1.75} aria-hidden=\"true\" />\n      <span className=\"sr-only\">{value ? includedLabel : excludedLabel}</span>\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/comparison-table.tsx"
    }
  ],
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}