{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stat-cards",
  "title": "Stat Cards",
  "author": "@fma-ds/ds",
  "description": "Totals above a list or on a dashboard: figures on their own or on cards that are static, pressable, links or filters.\n\n### `Stat`\n\nA label (`StatLabel`), a value (`StatValue`, formatted by the app, with `isLoading` and `isUnavailable` states), an optional `StatIcon` (a `soft` `IconTile`) and a `StatDescription`.\n\n- **`tone`**: `default`, `success`, `warning`, `info`, `destructive` or `muted`; tints the icon and the value.\n- **`size=\"lg\"`**: enlarges the value.\n\nOn its own, `Stat` has no surface, for several figures inside one card.\n\n### Cards\n\n- **`StatCard`**: static.\n- **`StatCardButton`**: runs `onPress`, or opens a dialog as the child of a `DialogTrigger`.\n- **`StatCardLink`**: navigates, e.g. to the list already filtered.\n- **`StatCardToggle`**: the selectable cards of a `StatCardGroup`, a react-aria `ToggleButtonGroup` that filters a list. `selectionMode=\"single\"` behaves like radios and `\"multiple\"` like checkboxes, controlled by `selectedKeys`/`onSelectionChange` so it maps to a search param.\n\n`StatGrid` and `StatCardGroup` lay cards out in as many columns as fit, and their `i18n.labels` reach every value inside.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "cn@^0.5.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/icon-tile",
    "@fma-ds/skeleton"
  ],
  "files": [
    {
      "path": "src/blocks/stat-cards/stat-cards.tsx",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport {\n  Button as ButtonPrimitive,\n  type ButtonProps as ButtonPrimitiveProps,\n  Link as LinkPrimitive,\n  type LinkProps as LinkPrimitiveProps,\n  ToggleButtonGroup,\n  type ToggleButtonGroupProps,\n  ToggleButton as ToggleButtonPrimitive,\n  type ToggleButtonProps as ToggleButtonPrimitiveProps,\n} from \"react-aria-components\";\nimport { IconTile } from \"@/core/icon-tile/icon-tile\";\nimport { Skeleton } from \"@/core/skeleton/skeleton\";\n\ntype StatLabels = {\n  /** Read by screen readers while a value loads. */\n  loading: string;\n  /** Read by screen readers in place of the \"—\" of an unavailable value. */\n  unavailable: string;\n};\n\ntype StatI18n = { labels?: Partial<StatLabels> };\n\nconst defaultStatLabels: StatLabels = {\n  loading: \"Loading\",\n  unavailable: \"Not available\",\n};\n\nconst StatLabelsContext = React.createContext<StatLabels>(defaultStatLabels);\n\nfunction useMergedLabels(i18n?: StatI18n) {\n  const overrides = i18n?.labels;\n  return React.useMemo(() => ({ ...defaultStatLabels, ...overrides }), [overrides]);\n}\n\n/**\n * The tone tints the icon with the status color and the value with its\n * `*-foreground`, which keeps text contrast on the card surface.\n */\nconst statVariants = cva(\n  \"group/stat grid min-w-0 grid-cols-[minmax(0,1fr)_auto] content-start items-start gap-x-3 gap-y-1 text-left\",\n  {\n    variants: {\n      tone: {\n        default: \"[--stat-icon:var(--primary)] [--stat-value:var(--foreground)]\",\n        success: \"[--stat-icon:var(--success)] [--stat-value:var(--success-foreground)]\",\n        warning: \"[--stat-icon:var(--warning)] [--stat-value:var(--warning-foreground)]\",\n        info: \"[--stat-icon:var(--info)] [--stat-value:var(--info-foreground)]\",\n        destructive:\n          \"[--stat-icon:var(--destructive)] [--stat-value:var(--destructive-foreground)]\",\n        muted: \"[--stat-icon:var(--muted-foreground)] [--stat-value:var(--muted-foreground)]\",\n      },\n    },\n    defaultVariants: { tone: \"default\" },\n  },\n);\n\ntype StatVariantProps = VariantProps<typeof statVariants> & {\n  /** `lg` enlarges the value, for a card that highlights a single figure. */\n  size?: \"default\" | \"lg\";\n};\n\nfunction statAttributes({ tone, size }: StatVariantProps) {\n  return { \"data-tone\": tone ?? \"default\", \"data-size\": size ?? \"default\" };\n}\n\n/** A label, a value and, optionally, an icon and a description. No surface of its own. */\nfunction Stat({ className, tone, size, ...props }: React.ComponentProps<\"div\"> & StatVariantProps) {\n  return (\n    <div\n      data-slot=\"stat\"\n      {...statAttributes({ tone, size })}\n      className={cn(statVariants({ tone }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction StatLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"stat-label\"\n      className={cn(\"col-start-1 text-sm font-medium text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * The figure, formatted by the app. `isLoading` shows a skeleton and\n * `isUnavailable` a \"—\" (e.g. a metric that isn't offered yet); both are\n * announced through `i18n.labels`.\n */\nfunction StatValue({\n  className,\n  isLoading,\n  isUnavailable,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & { isLoading?: boolean; isUnavailable?: boolean }) {\n  const labels = React.useContext(StatLabelsContext);\n  let content = children;\n  if (isLoading) {\n    content = (\n      <>\n        <Skeleton aria-hidden=\"true\" className=\"h-[1lh] w-16 max-w-full\" />\n        <span className=\"sr-only\">{labels.loading}</span>\n      </>\n    );\n  } else if (isUnavailable) {\n    content = (\n      <>\n        <span aria-hidden=\"true\">—</span>\n        <span className=\"sr-only\">{labels.unavailable}</span>\n      </>\n    );\n  }\n  return (\n    <div\n      data-slot=\"stat-value\"\n      aria-busy={isLoading || undefined}\n      className={cn(\n        \"col-start-1 font-heading text-2xl leading-tight font-semibold text-(--stat-value) tabular-nums group-data-[size=lg]/stat:text-3xl\",\n        isUnavailable && !isLoading && \"text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {content}\n    </div>\n  );\n}\n\nfunction StatDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"stat-description\"\n      className={cn(\"col-span-full text-xs text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The icon, in a `soft` `IconTile` tinted by the stat's tone. */\nfunction StatIcon({ className, ...props }: React.ComponentProps<typeof IconTile>) {\n  return (\n    <IconTile\n      data-slot=\"stat-icon\"\n      aria-hidden=\"true\"\n      variant=\"soft\"\n      size=\"sm\"\n      className={cn(\"col-start-2 row-span-2 row-start-1 text-(--stat-icon)\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Lays stats or stat cards out in as many columns as fit (each at least\n * `13rem`), so it works in a page or inside a card without breakpoints. Its\n * `i18n` reaches every value inside.\n */\nfunction StatGrid({\n  className,\n  i18n,\n  ...props\n}: React.ComponentProps<\"div\"> & { i18n?: StatI18n }) {\n  const labels = useMergedLabels(i18n);\n  return (\n    <StatLabelsContext.Provider value={labels}>\n      <div data-slot=\"stat-grid\" className={cn(statGridClassName, className)} {...props} />\n    </StatLabelsContext.Provider>\n  );\n}\n\nconst statGridClassName =\n  \"grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] items-stretch gap-4\";\n\nconst statCardClassName =\n  \"rounded-xl bg-card p-4 text-sm text-card-foreground ring-1 ring-foreground/10\";\n\n/** States shared by the pressable cards: hover, press, keyboard focus, disabled. */\nconst interactiveCardClassName = [\n  \"cursor-default outline-none transition-[background-color,box-shadow]\",\n  \"data-hovered:bg-accent/50 data-pressed:bg-accent\",\n  \"data-focus-visible:ring-3 data-focus-visible:ring-ring/50\",\n  \"data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n];\n\n/** A static stat on a card surface: the totals above a list or a dashboard. */\nfunction StatCard({\n  className,\n  tone,\n  size,\n  ...props\n}: React.ComponentProps<\"div\"> & StatVariantProps) {\n  return (\n    <div\n      data-slot=\"stat-card\"\n      {...statAttributes({ tone, size })}\n      className={cn(statVariants({ tone }), statCardClassName, className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * A stat card that does something when pressed: open a dialog or popover\n * (as the child of a `DialogTrigger`), run a callback, etc.\n */\nfunction StatCardButton({\n  className,\n  tone,\n  size,\n  ...props\n}: Omit<ButtonPrimitiveProps, \"className\"> & { className?: string } & StatVariantProps) {\n  return (\n    <ButtonPrimitive\n      data-slot=\"stat-card\"\n      {...statAttributes({ tone, size })}\n      className={cn(statVariants({ tone }), statCardClassName, interactiveCardClassName, className)}\n      {...props}\n    />\n  );\n}\n\n/** A stat card that navigates, e.g. to the list already filtered by it. */\nfunction StatCardLink({\n  className,\n  tone,\n  size,\n  ...props\n}: Omit<LinkPrimitiveProps, \"className\"> & { className?: string } & StatVariantProps) {\n  return (\n    <LinkPrimitive\n      data-slot=\"stat-card\"\n      {...statAttributes({ tone, size })}\n      className={cn(statVariants({ tone }), statCardClassName, interactiveCardClassName, className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Stat cards that filter a list. A react-aria `ToggleButtonGroup`:\n * `selectionMode=\"single\"` behaves like radios (one total at a time; add\n * `disallowEmptySelection` to keep one picked), `\"multiple\"` like checkboxes.\n * Controlled with `selectedKeys`/`onSelectionChange`, so the selection maps\n * to a search param. Laid out like `StatGrid`.\n */\nfunction StatCardGroup({\n  className,\n  i18n,\n  children,\n  ...props\n}: Omit<ToggleButtonGroupProps, \"className\" | \"children\"> & {\n  className?: string;\n  i18n?: StatI18n;\n  children?: React.ReactNode;\n}) {\n  const labels = useMergedLabels(i18n);\n  return (\n    <ToggleButtonGroup\n      data-slot=\"stat-card-group\"\n      className={cn(statGridClassName, className)}\n      {...props}\n    >\n      <StatLabelsContext.Provider value={labels}>{children}</StatLabelsContext.Provider>\n    </ToggleButtonGroup>\n  );\n}\n\n/**\n * One selectable card of a `StatCardGroup`, identified by `id`. Selected, it\n * gets the `accent` surface and a primary ring, thicker than the resting one,\n * so the state doesn't rely on color alone.\n */\nfunction StatCardToggle({\n  className,\n  tone,\n  size,\n  ...props\n}: Omit<ToggleButtonPrimitiveProps, \"className\"> & { className?: string } & StatVariantProps) {\n  return (\n    <ToggleButtonPrimitive\n      data-slot=\"stat-card\"\n      {...statAttributes({ tone, size })}\n      className={cn(\n        statVariants({ tone }),\n        statCardClassName,\n        interactiveCardClassName,\n        \"data-selected:bg-accent data-selected:ring-2 data-selected:ring-primary\",\n        \"data-selected:data-focus-visible:ring-3 data-selected:data-focus-visible:ring-primary\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type { StatI18n, StatLabels };\nexport {\n  Stat,\n  StatCard,\n  StatCardButton,\n  StatCardGroup,\n  StatCardLink,\n  StatCardToggle,\n  StatDescription,\n  StatGrid,\n  StatIcon,\n  StatLabel,\n  StatValue,\n  statVariants,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/stat-cards/stat-cards.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "stats",
      "dashboard",
      "totals",
      "filters"
    ],
    "contentHash": "69acf80190a0"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}