{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "panel-card",
  "title": "Panel Card",
  "author": "@fma-ds/ds",
  "description": "A panel on a `Card` — a dashboard chart or table, or one resource and its status (a contract, the current plan) — with a standard header and a body that handles its own states.\n\n### `PanelCardHeader`\n\n- **`title`**: a heading, `headingLevel` 2 by default.\n- **`icon`**: shown in a `soft` `IconTile`.\n- **`description`** and **`status`**: a `Badge` next to the title.\n- **`help`**: opened by an ⓘ button in a popover, so it works with touch and keyboard.\n- **`updatedAt`**: a `Date` or timestamp, formatted with `Intl` in the react-aria locale; `isRefreshing` shows a spinner next to it while the content stays on screen.\n- **`toolbar`**: a slot for the card's own controls, which wraps below the title on narrow cards.\n\n### `PanelCardBody`\n\nShows the first state that applies — `isLoading` (skeleton, first load only), `isError` (message plus \"Try again\" with `onRetry`), `isEmpty` (`Empty`) — or its children. `emptyState`/`errorState` replace the built-in messages (e.g. a missing resource with the action that adds it).\n\n### What goes where\n\n- **Body**: charts, tables, `stat-cards` and `description-list`; problems with the resource go in an `Alert` there, not in the card colors.\n- **`CardFooter`**: actions.\n\n`PanelCard`'s `i18n.labels` reaches every built-in text.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/card",
    "@fma-ds/empty",
    "@fma-ds/field",
    "@fma-ds/icon-tile",
    "@fma-ds/skeleton",
    "@fma-ds/spinner"
  ],
  "files": [
    {
      "path": "src/blocks/panel-card/panel-card.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { CircleAlertIcon, InboxIcon, RefreshCwIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useLocale } from \"react-aria-components\";\nimport { Button } from \"@/core/button/button\";\nimport { Card, CardHeader, CardTitle } from \"@/core/card/card\";\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/core/empty/empty\";\nimport { FieldHelp } from \"@/core/field/field\";\nimport { IconTile } from \"@/core/icon-tile/icon-tile\";\nimport { Skeleton } from \"@/core/skeleton/skeleton\";\nimport { Spinner } from \"@/core/spinner/spinner\";\n\ntype PanelCardLabels = {\n  /** The help button's accessible name; a text `title` is appended (\"More information: Sales\"). */\n  help: string;\n  /** The \"last updated\" line, given the formatted date. */\n  updatedAt: (formatted: string) => string;\n  formatDate: (date: Date, locale: string) => string;\n  /** Announced while the card refetches with its content still on screen. */\n  refreshing: string;\n  loading: string;\n  empty: string;\n  emptyDescription: string;\n  error: string;\n  errorDescription: string;\n  retry: string;\n};\n\ntype PanelCardI18n = { labels?: Partial<PanelCardLabels> };\n\nconst defaultPanelCardLabels: PanelCardLabels = {\n  help: \"More information\",\n  updatedAt: (formatted) => `Updated ${formatted}`,\n  formatDate: (date, locale) =>\n    new Intl.DateTimeFormat(locale, { dateStyle: \"short\", timeStyle: \"short\" }).format(date),\n  refreshing: \"Updating\",\n  loading: \"Loading\",\n  empty: \"No data\",\n  emptyDescription: \"There's nothing to show for this period yet.\",\n  error: \"Couldn't load the data\",\n  errorDescription: \"Check your connection and try again.\",\n  retry: \"Try again\",\n};\n\nconst PanelCardContext = React.createContext<PanelCardLabels>(defaultPanelCardLabels);\n\nconst usePanelCardLabels = () => React.useContext(PanelCardContext);\n\n/**\n * A `Card` for a panel: a dashboard chart or table, or one resource and its\n * status (a contract, the current plan). Its `i18n` reaches the header and\n * body inside.\n */\nfunction PanelCard({\n  i18n,\n  className,\n  ...props\n}: React.ComponentProps<typeof Card> & { i18n?: PanelCardI18n }) {\n  const overrides = i18n?.labels;\n  const labels = React.useMemo(() => ({ ...defaultPanelCardLabels, ...overrides }), [overrides]);\n  return (\n    <PanelCardContext.Provider value={labels}>\n      <Card data-slot=\"panel-card\" className={cn(\"min-w-0\", className)} {...props} />\n    </PanelCardContext.Provider>\n  );\n}\n\n/**\n * The standard header: icon, title (a heading, `headingLevel` 2 by default)\n * with an optional help button and `status` (a `Badge`), description, the\n * \"last updated\" line and a toolbar slot that wraps below the title on narrow\n * cards. `help` opens in a\n * popover, so it works with touch and keyboard. `isRefreshing` spins next to\n * the date while the content stays on screen.\n */\nfunction PanelCardHeader({\n  title,\n  icon,\n  description,\n  help,\n  status,\n  updatedAt,\n  isRefreshing,\n  toolbar,\n  headingLevel = 2,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  title: React.ReactNode;\n  icon?: React.ReactNode;\n  description?: React.ReactNode;\n  help?: React.ReactNode;\n  /** The resource's status, next to the title: a `Badge`. */\n  status?: React.ReactNode;\n  updatedAt?: Date | number;\n  isRefreshing?: boolean;\n  toolbar?: React.ReactNode;\n  headingLevel?: 2 | 3 | 4 | 5 | 6;\n}) {\n  const labels = usePanelCardLabels();\n  const { locale } = useLocale();\n  const date = updatedAt === undefined ? undefined : new Date(updatedAt);\n  return (\n    <CardHeader\n      data-slot=\"panel-card-header\"\n      className={cn(\"flex flex-wrap items-start justify-between gap-3\", className)}\n      {...props}\n    >\n      <div className=\"flex min-w-0 flex-1 basis-56 items-start gap-3\">\n        {icon && (\n          <IconTile aria-hidden=\"true\" variant=\"soft\" size=\"sm\">\n            {icon}\n          </IconTile>\n        )}\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <div className=\"flex min-h-8 flex-wrap items-center gap-x-1 gap-y-0.5\">\n            <CardTitle role=\"heading\" aria-level={headingLevel}>\n              {title}\n            </CardTitle>\n            {help && (\n              <FieldHelp\n                label={typeof title === \"string\" ? `${labels.help}: ${title}` : labels.help}\n              >\n                {help}\n              </FieldHelp>\n            )}\n            {status && (\n              <span data-slot=\"panel-card-status\" className=\"ml-1 inline-flex\">\n                {status}\n              </span>\n            )}\n          </div>\n          {description && (\n            <div data-slot=\"panel-card-description\" className=\"text-sm text-muted-foreground\">\n              {description}\n            </div>\n          )}\n          {(date || isRefreshing) && (\n            <div\n              data-slot=\"panel-card-updated-at\"\n              className=\"flex items-center gap-1.5 text-xs text-muted-foreground\"\n            >\n              {date && (\n                <time dateTime={date.toISOString()}>\n                  {labels.updatedAt(labels.formatDate(date, locale))}\n                </time>\n              )}\n              {isRefreshing && <Spinner aria-label={labels.refreshing} className=\"size-3\" />}\n            </div>\n          )}\n        </div>\n      </div>\n      {toolbar && (\n        <div\n          data-slot=\"panel-card-toolbar\"\n          className=\"flex flex-wrap items-center justify-end gap-2\"\n        >\n          {toolbar}\n        </div>\n      )}\n    </CardHeader>\n  );\n}\n\n/**\n * The content area, which swaps itself for the first state that applies:\n * `isLoading` (a skeleton — the first load only; refetches use the header's\n * `isRefreshing`), `isError` (a message, with \"Try again\" when `onRetry` is\n * given), `isEmpty`, or the children. `emptyState`/`errorState` replace the\n * built-in messages. Give it a `min-h-*` to keep the card's height steady.\n */\nfunction PanelCardBody({\n  isLoading,\n  isEmpty,\n  isError,\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  isLoading?: boolean;\n  isEmpty?: boolean;\n  isError?: boolean;\n  onRetry?: () => void;\n  emptyState?: React.ReactNode;\n  errorState?: React.ReactNode;\n}) {\n  const labels = usePanelCardLabels();\n  let content = children;\n  if (isLoading) {\n    content = (\n      <>\n        <Skeleton aria-hidden=\"true\" className=\"min-h-40 flex-1\" />\n        <span className=\"sr-only\">{labels.loading}</span>\n      </>\n    );\n  } else if (isError) {\n    content = errorState ?? (\n      <Empty role=\"alert\" className=\"p-4\">\n        <EmptyHeader>\n          <EmptyMedia variant=\"icon\" className=\"bg-destructive/10 text-destructive\">\n            <CircleAlertIcon />\n          </EmptyMedia>\n          <EmptyTitle>{labels.error}</EmptyTitle>\n          <EmptyDescription>{labels.errorDescription}</EmptyDescription>\n        </EmptyHeader>\n        {onRetry && (\n          <EmptyContent>\n            <Button variant=\"outline\" size=\"sm\" onPress={onRetry}>\n              <RefreshCwIcon data-icon=\"inline-start\" />\n              {labels.retry}\n            </Button>\n          </EmptyContent>\n        )}\n      </Empty>\n    );\n  } else if (isEmpty) {\n    content = emptyState ?? (\n      <Empty className=\"p-4\">\n        <EmptyHeader>\n          <EmptyMedia variant=\"icon\">\n            <InboxIcon />\n          </EmptyMedia>\n          <EmptyTitle>{labels.empty}</EmptyTitle>\n          <EmptyDescription>{labels.emptyDescription}</EmptyDescription>\n        </EmptyHeader>\n      </Empty>\n    );\n  }\n  return (\n    <div\n      data-slot=\"panel-card-body\"\n      aria-busy={isLoading || undefined}\n      className={cn(\"flex min-w-0 flex-1 flex-col px-(--card-spacing)\", className)}\n      {...props}\n    >\n      {content}\n    </div>\n  );\n}\n\nexport type { PanelCardI18n, PanelCardLabels };\nexport { PanelCard, PanelCardBody, PanelCardHeader };\n",
      "type": "registry:component",
      "target": "components/fma-ds/panel-card/panel-card.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "dashboard",
      "card",
      "chart",
      "status"
    ],
    "contentHash": "293c6339cf41"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}