{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-card",
  "title": "Table Card",
  "description": "Card shell around a table: header, body, footer, count and a bulk-action bar.",
  "dependencies": [
    "lucide-react@^1.27.0"
  ],
  "registryDependencies": [
    "@pestoui/badge",
    "@pestoui/button",
    "@pestoui/card",
    "@pestoui/number-animation",
    "@pestoui/style",
    "@pestoui/table",
    "@pestoui/utils"
  ],
  "files": [
    {
      "path": "pestoui/ui/table-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardDescription, CardTitle } from \"@/components/ui/card\"\nimport { NumberTicker } from \"@/components/ui/number-animation\"\nimport { tableDensity } from \"@/components/ui/table\"\n\n/* ---------------------------------------------------------------------------\n   The shell around a table: header block, flush body, footer, bulk-action bar.\n\n   Separate from `table.tsx` on purpose. This file reaches for card, badge,\n   button and the ticker; the primitive file reaches for almost nothing. Anyone\n   who wants a bare styled table copies `table.tsx` and takes none of this, and\n   a future state-owning data table can sit alongside both without touching\n   either.\n\n   Same boundary as the primitives: nothing here holds state. `open`, `count`\n   and the page numbers in the footer all come from the caller.\n--------------------------------------------------------------------------- */\n\ninterface TableCardProps\n  extends Omit<React.ComponentProps<typeof Card>, \"size\"> {\n  /** One owner for density. Sets the card's own spacing *and* the table's cell\n   *  padding, so the header, the rows and the footer can never end up on\n   *  different scales. A `<Table size>` inside still wins if you need it to. */\n  size?: \"default\" | \"sm\"\n}\n\n/* `gap-0 py-0` because the header and footer own their vertical padding here —\n   the table has to sit flush against the card's rounded edge, and the card's\n   default column gap would open a seam between the header hairline and the\n   first row. */\nfunction TableCard({ className, size = \"default\", ...props }: TableCardProps) {\n  return (\n    <Card\n      data-slot=\"table-card\"\n      size={size}\n      className={cn(\"gap-0 py-0\", tableDensity[size], className)}\n      {...props}\n    />\n  )\n}\n\n/* A grid rather than a flex row so the title, the description and the action\n   cluster place themselves — no wrapper div around the left-hand column, and a\n   header with no description collapses to one row on its own. */\nfunction TableCardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"table-card-header\"\n      className={cn(\n        \"grid auto-rows-min grid-cols-[1fr_auto] items-start gap-x-4 gap-y-1 border-b p-(--card-spacing)\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* Composes `CardTitle` rather than restating it — the count pill needs the\n   title to be a flex row, and that is the only difference. */\nfunction TableCardTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof CardTitle>) {\n  return (\n    <CardTitle\n      className={cn(\"col-start-1 flex items-center gap-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction TableCardDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof CardDescription>) {\n  return <CardDescription className={cn(\"col-start-1\", className)} {...props} />\n}\n\n/* The pill beside the title. Secondary, not the count variant — that one is\n   filled with `--info`, and blue in this system means informational, never\n   \"here is a quantity\". */\nfunction TableCardCount({\n  className,\n  variant = \"secondary\",\n  ...props\n}: React.ComponentProps<typeof Badge>) {\n  return (\n    <Badge\n      data-slot=\"table-card-count\"\n      variant={variant}\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction TableCardActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"table-card-actions\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 flex items-center gap-2 self-start justify-self-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* Zero padding — the table draws its own edge inset from the density vars — and\n   `relative`, which is what the bulk bar anchors to. */\nfunction TableCardBody({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"table-card-body\"\n      className={cn(\"relative\", className)}\n      {...props}\n    />\n  )\n}\n\n/* A distinct slot name from `card-footer` on purpose: `Card` zeroes its own\n   bottom padding when it sees a `card-footer` descendant, and inheriting that\n   rule here would fight the `py-0` this shell already sets.\n *\n * Drop the result count on the left and `<Pagination>` on the right. Pagination\n * defaults to `mx-auto w-full justify-center`, so it needs\n * `className=\"mx-0 w-auto justify-end\"` here or it centres itself in the gap. */\nfunction TableCardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"table-card-footer\"\n      className={cn(\n        \"flex flex-wrap items-center justify-between gap-4 border-t bg-muted/50 p-(--card-spacing)\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* ---------------------------------------------------------------------------\n   Bulk-action bar\n--------------------------------------------------------------------------- */\n\ninterface TableBulkBarProps extends React.ComponentProps<\"div\"> {\n  /** Caller-computed selection size. */\n  count: number\n  open: boolean\n  onClear?: () => void\n  /** Singular and plural noun for the count, e.g. `[\"row\", \"rows\"]`. */\n  label?: [string, string]\n}\n\n/* Anchored to `TableCardBody`, not portalled. A portal would land it at the end\n   of the document and therefore at the end of the tab order, which means a\n   keyboard user who just checked three boxes has to tab past the whole page to\n   reach the actions — and it would need a focus trap to be usable at all.\n   Anchoring costs one `position: relative` and puts the bar exactly where the\n   tab order expects it: immediately after the rows it acts on.\n *\n * `--inverse` rather than another card surface: the bar has to read as\n * something laid over the table, not as one more row in it.\n *\n * Enter and exit are chosen classes rather than a self-read `data-state`, so\n * nothing here styles itself from its own attributes. The attribute is still\n * emitted for descendants and for tests. */\nfunction TableBulkBar({\n  className,\n  count,\n  open,\n  onClear,\n  label = [\"row\", \"rows\"],\n  children,\n  ...props\n}: TableBulkBarProps) {\n  /* Held mounted through the exit so the closing animation has something to\n     run on; the animation itself reports when it is safe to drop. */\n  const [mounted, setMounted] = React.useState(open)\n\n  React.useEffect(() => {\n    if (open) setMounted(true)\n  }, [open])\n\n  if (!mounted) return null\n\n  return (\n    /* The positioning wrapper exists because `animate-in slide-in-from-*`\n       writes the whole transform. A centring `-translate-x-1/2` on the animated\n       element would be overwritten for the length of the entrance and the bar\n       would slide in diagonally. Centring with flex keeps the two concerns on\n       separate elements. */\n    <div\n      aria-hidden={!open}\n      className=\"pointer-events-none absolute inset-x-0 bottom-[var(--card-spacing,calc(var(--spacing)*4))] z-[var(--z-sticky,1)] flex justify-center px-[var(--card-spacing,calc(var(--spacing)*4))]\"\n    >\n      <div\n        data-slot=\"table-bulk-bar\"\n        data-state={open ? \"open\" : \"closed\"}\n        role=\"status\"\n        aria-live=\"polite\"\n        onAnimationEnd={(event) => {\n          if (event.target === event.currentTarget && !open) setMounted(false)\n        }}\n        className={cn(\n          \"pointer-events-auto flex items-center gap-1 rounded-full bg-inverse py-1.5 pr-1.5 pl-4 text-inverse-foreground shadow-float\",\n          /* Overlay in, so the panel spring. Out on a short tween travelling\n             half the distance — exits are shorter than enters — and no blur,\n             which is an enter-only effect. `anim-*` rather than `duration-*`\n             keeps the long entrance from retiming the button colour\n             transitions inside the pill. */\n          open\n            ? \"animate-in fade-in slide-in-from-bottom-4 blur-in-[3px] anim-spring-panel ease-spring-panel\"\n            : \"animate-out fade-out slide-out-to-bottom-2 anim-exit ease-out-expo\",\n          className\n        )}\n        {...props}\n      >\n        <span className=\"flex items-center gap-1.5 text-sm font-medium\">\n          <NumberTicker value={count} className=\"tabular-nums\" />\n          <span className=\"text-inverse-muted\">\n            {count === 1 ? label[0] : label[1]} selected\n          </span>\n        </span>\n        {children ? (\n          <>\n            <span\n              aria-hidden=\"true\"\n              className=\"mx-1 h-5 w-px shrink-0 bg-inverse-foreground/15\"\n            />\n            {children}\n          </>\n        ) : null}\n        {onClear ? (\n          <TableBulkBarAction\n            size=\"icon-sm\"\n            onClick={onClear}\n            aria-label=\"Clear selection\"\n          >\n            <X />\n          </TableBulkBarAction>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\n/* A `Button` retinted for the inverse pill. `variant=\"ghost\"` alone would keep\n   its light-surface hover fill and go invisible against the dark bar, so the\n   hover state is restated against `--inverse-foreground` instead. */\nfunction TableBulkBarAction({\n  className,\n  variant = \"ghost\",\n  size = \"sm\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      className={cn(\n        \"text-inverse-foreground hover:bg-inverse-foreground/10 hover:text-inverse-foreground dark:hover:bg-inverse-foreground/10\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  TableCard,\n  TableCardHeader,\n  TableCardTitle,\n  TableCardDescription,\n  TableCardCount,\n  TableCardActions,\n  TableCardBody,\n  TableCardFooter,\n  TableBulkBar,\n  TableBulkBarAction,\n}\n\nexport type { TableCardProps, TableBulkBarProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}