{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"card","title":"Card","description":"Structured application surface for related content and actions.","dependencies":["@base-ui/react@1.7.0"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/card/card.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport type React from \"react\";\nimport { useResolvedDensity } from \"../../system/provider/siteplane-ui-provider\";\nimport { surfaceClassNames } from \"../../system/styles/surface\";\nimport { cn } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\n\nexport function Card({\n  className,\n  density,\n  render,\n  variant = \"raised\",\n  ...props\n}: useRender.ComponentProps<\"div\"> & {\n  /**\n   * The inset of the card's parts: 20 px, or 16 px compact. Without it the\n   * card takes the density of the SiteplaneUIProvider around it. A theme's\n   * --sp-rw-card-inset, --sp-rw-card-inset-block or --sp-rw-card-inset-inline\n   * takes the place of the density's inset (design-rework.css).\n   */\n  density?: \"default\" | \"compact\" | undefined;\n  variant?:\n    | \"raised\"\n    | \"subtle\"\n    | \"header\"\n    | \"message\"\n    | \"error\"\n    | \"inset\"\n    | \"elevated\"\n    | \"glass\";\n}): React.ReactElement {\n  const resolvedDensity = useResolvedDensity(density);\n  const defaultProps = {\n    // A card nested in a framed card keeps the default inset of its parts: the\n    // frame's inset (CardFrame) reaches only the cards joined into it.\n    className: cn(\n      // Glass is a surface of its own: the shared glass rule draws its fill,\n      // edge, text colour and blur instead of the raised surface.\n      slotClass(\"card\", props, render),\n      baseClass(\"card\", props, render),\n      variant === \"glass\" ? surfaceClassNames.glass : surfaceClassNames.raised,\n      \"flex flex-col [--card-inset-inline:initial]\",\n      className,\n    ),\n    \"data-density\": resolvedDensity,\n    // Stays when the Card renders as another part and takes its slot (a\n    // ContextMenu trigger), so the Card's base radius still reaches it.\n    \"data-foundation\": \"card\",\n    \"data-slot\": \"card\",\n    \"data-variant\": variant,\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrame({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    // A joined card's header, panel and footer take the frame's inset, like a\n    // table's outer columns, so their text starts with the frame header's; the\n    // extra pixel offsets the card's negative margin.\n    className: cn(\n      slotClass(\"card-frame\", props, render),\n      surfaceClassNames.raised,\n      \"flex flex-col [--card-frame-inset:--spacing(6)] [--clip-bottom:-1rem] [--clip-top:-1rem] before:rounded-[inherit] *:data-[slot=card]:[--card-inset-inline:calc(var(--card-frame-inset)+1px)] *:data-[slot=card]:[--table-edge-padding:calc(var(--card-frame-inset)+1px)] *:data-[slot=card]:-m-px *:data-[slot=card]:before:hidden *:not-first:data-[slot=card]:before:rounded-t-[inherit] *:not-last:data-[slot=card]:before:rounded-b-[inherit] *:data-[slot=card]:[clip-path:inset(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--sp-rw-radius-surface)-1px))] *:data-[slot=card]:last:[--clip-bottom:1px] *:data-[slot=card]:first:[--clip-top:1px]\",\n      className,\n    ),\n    \"data-slot\": \"card-frame\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameBleed({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    // Tables inside align their outer columns with the frame header and footer;\n    // the extra pixel offsets the negative margin that covers the frame border.\n    className: cn(\n      slotClass(\"card-frame-bleed\", props, render),\n      \"-m-px w-[calc(100%+2px)] [--table-edge-padding:calc(var(--card-frame-inset)+1px)]\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-bleed\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameHeader({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-frame-header\", props, render),\n      \"relative flex grid auto-rows-min grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto] flex-col items-start gap-x-4 px-[var(--card-frame-inset,--spacing(6))] py-4 has-data-[slot=card-frame-action]:grid-cols-[minmax(0,1fr)_auto]\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-header\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameTitle({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-frame-title\", props, render),\n      \"self-center font-(--sp-rw-font-weight-strong) text-sm wrap-anywhere\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-title\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameDescription({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-frame-description\", props, render),\n      \"self-center text-muted-foreground text-sm wrap-anywhere\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-description\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameAction({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-frame-action\", props, render),\n      \"col-start-2 nth-3:row-span-2 nth-3:row-start-1 inline-flex self-center justify-self-end\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-action\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFrameFooter({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-frame-footer\", props, render),\n      \"px-[var(--card-frame-inset,--spacing(6))] py-4\",\n      className,\n    ),\n    \"data-slot\": \"card-frame-footer\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardHeader({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  // The text parts share one column; a media (CardMedia) takes a column of its\n  // own before it and an action (CardAction) one after it. The text parts are\n  // the children that are neither of the two. How the parts align in the rows\n  // follows from how many text parts the header holds, which design-rework.css\n  // decides in one block of its Card section.\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-header\", props, render),\n      \"grid auto-rows-min grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto] has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto] has-data-[slot=card-media]:grid-cols-[auto_minmax(0,1fr)] has-data-[slot=card-media]:has-data-[slot=card-action]:grid-cols-[auto_minmax(0,1fr)_auto]\",\n      className,\n    ),\n    \"data-slot\": \"card-header\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\n/**\n * The leading avatar or icon of a header, before its title and description\n * (rulebook I7). A bare icon is 20 px and centres on the title's first line; an\n * avatar taller than that line starts at its top (I3, I5). Beside a title\n * alone it centres on the title, as CardAction does.\n */\nexport function CardMedia({\n  className,\n  render,\n  tone = \"accent\",\n  ...props\n}: useRender.ComponentProps<\"div\"> & {\n  /**\n   * The color of a bare icon: `accent`, or `neutral` for the secondary text\n   * color (--sp-rw-fg-secondary), for a module icon that leaves color to\n   * states. An avatar or an image keeps its own colors.\n   */\n  tone?: \"accent\" | \"neutral\" | undefined;\n}): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-media\", props, render),\n      \"col-start-1 row-start-1 flex\",\n      className,\n    ),\n    \"data-slot\": \"card-media\",\n    \"data-tone\": tone,\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardTitle({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-title\", props, render),\n      \"wrap-anywhere\",\n      className,\n    ),\n    \"data-slot\": \"card-title\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\n/**\n * Context beside the title, in its row: a period, a count or a short status.\n * Place it inside CardTitle, after the title's text. It takes the quieter text\n * of CardDescription and moves below the title when the row is full.\n */\nexport function CardTitleMeta({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"span\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-title-meta\", props, render),\n      \"min-w-0 wrap-anywhere\",\n      className,\n    ),\n    \"data-slot\": \"card-title-meta\",\n  };\n\n  return useRender({\n    defaultTagName: \"span\",\n    props: mergeProps<\"span\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardDescription({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-description\", props, render),\n      \"wrap-anywhere\",\n      className,\n    ),\n    \"data-slot\": \"card-description\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardAction({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  // Beside a title and a description the action spans both rows from the top.\n  // Beside one text part it stays in that part's row and centres on it, as\n  // CardFrameAction does, so it adds no second row (rulebook S2, S11); the\n  // Card section of design-rework.css owns both. It takes the last column,\n  // after the text and after a media's column before it.\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-action\", props, render),\n      \"-col-start-2 row-start-1 inline-flex justify-self-end\",\n      className,\n    ),\n    \"data-slot\": \"card-action\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardPanel({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(slotClass(\"card-panel\", props, render), \"flex-1\", className),\n    \"data-slot\": \"card-panel\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function CardFooter({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"card-footer\", props, render),\n      \"flex items-center\",\n      className,\n    ),\n    \"data-slot\": \"card-footer\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport { CardPanel as CardContent };\n","type":"registry:ui","target":"~/src/siteplane/components/card/card.tsx"},{"path":"packages/ui/src/components/card/index.ts","content":"export * from \"./card\";\n","type":"registry:ui","target":"~/src/siteplane/components/card/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/card.md","categories":["Data display"],"meta":{"avoidWhen":["Avoid turning a card that contains buttons into one large button; keep the controls separate.","Avoid for raised panels that share one surface; use Frame."],"description":"Structured application surface for related content and actions.","knownLimitations":["The title parts render divs. Pass render={<h2 />} when the title belongs in the document outline."],"providerNotes":["Card takes the density of SiteplaneUIProvider: \"compact\" pads its parts by 16 px instead of 20 px. The density prop of a card wins over the provider, and the --sp-rw-card-inset tokens win over both."],"releaseStatus":"stable","searchTerms":["card"],"useWhen":["Use to group related content and actions on one surface, with an optional header, content and footer."]}}