{
  "id": "card",
  "name": "Card",
  "description": "A surface that groups related content, such as a title, a short text and one action.",
  "stacks": [
    "react",
    "angular",
    "vue",
    "swift",
    "kotlin"
  ],
  "spec": {
    "$schema": "./schema/component-spec.schema.json",
    "name": "Card",
    "description": "A surface that groups related content, such as a title, a short text and one action.",
    "source": "components/lab/demo-components.tsx (styles: RECIPES.card in lib/lab/playground.ts)",
    "element": "article",
    "extends": "HTMLAttributes<HTMLElement>",
    "whenToUse": [
      "Group related content with at most one main action: a project, an article, a setting summary.",
      "Show several items of the same kind side by side, each in its own Card."
    ],
    "whenNotToUse": [
      "As a clickable surface: Card has no button role or keyboard handling. Put a link or a Button inside it instead.",
      "For page layout or whole sections: use plain layout elements."
    ],
    "props": [
      {
        "name": "variant",
        "type": "enum",
        "values": [
          "elevated",
          "outlined",
          "filled"
        ],
        "default": "elevated",
        "required": false,
        "description": "Surface style: shadow, hairline border or a tint of color.primary."
      },
      {
        "name": "size",
        "type": "enum",
        "values": [
          "sm",
          "md",
          "lg"
        ],
        "default": "md",
        "required": false,
        "description": "Padding and maximum width, from RECIPES.card.sizes."
      },
      {
        "name": "disabled",
        "type": "boolean",
        "default": false,
        "required": false,
        "description": "Sets aria-disabled, dims the card to 50% and blocks pointer events."
      },
      {
        "name": "children",
        "type": "node",
        "default": null,
        "required": false,
        "description": "The content, styled with cardParts (eyebrow, title, body, action)."
      },
      {
        "name": "className",
        "type": "string",
        "default": null,
        "required": false,
        "description": "Extra classes, merged after the recipe with cn()."
      }
    ],
    "variants": [
      "elevated",
      "outlined",
      "filled"
    ],
    "sizes": [
      "sm",
      "md",
      "lg"
    ],
    "states": [
      {
        "name": "default",
        "description": "elevated: color.surface with a soft shadow. outlined: transparent with a color.border hairline. filled: color.surface tinted 10% with color.primary."
      },
      {
        "name": "hover",
        "description": "elevated: deeper shadow. outlined: the border turns color.primary. filled: the tint grows to 16%. data-force=\"hover\" pins this state for docs and visual tests."
      },
      {
        "name": "disabled",
        "description": "aria-disabled=\"true\", 50% opacity and no pointer events."
      }
    ],
    "tokens": [
      {
        "path": "color.surface",
        "usage": "elevated background, and the base of the filled tint."
      },
      {
        "path": "color.text",
        "usage": "Text colour."
      },
      {
        "path": "color.muted",
        "usage": "cardParts.body text."
      },
      {
        "path": "color.primary",
        "usage": "Eyebrow and action text, outlined hover border, filled tint, action focus ring."
      },
      {
        "path": "color.border",
        "usage": "outlined border."
      },
      {
        "path": "radius.card",
        "usage": "Corner radius; the action's focus ring uses half of it."
      },
      {
        "path": "space.sm",
        "usage": "Padding at size sm (the web recipe's p-4 has the same value)."
      },
      {
        "path": "space.md",
        "usage": "Padding at size md (p-5, same value)."
      },
      {
        "path": "space.lg",
        "usage": "Padding at size lg (p-6, same value)."
      },
      {
        "path": "fontSize.title",
        "usage": "cardParts.title text (text-base, same value)."
      },
      {
        "path": "fontSize.body",
        "usage": "cardParts.body and cardParts.action text (text-sm, same value)."
      }
    ],
    "accessibility": {
      "role": "article, from the native <article> element. The Card itself is not interactive.",
      "keyboard": [
        "The Card is not focusable; Tab reaches the links or buttons inside it.",
        "When disabled, also take inner actions out of the tab order (tabIndex={-1}): aria-disabled and pointer-events do not do that."
      ],
      "focus": "The action (cardParts.action) shows a 2px color.primary ring on focus-visible. Inner Buttons keep their own ring.",
      "contrast": "Title and body text at least 4.5:1 against the card background in every variant (WCAG 2.2 AA, 1.4.3).",
      "labels": "Start the content with a heading (cardParts.title) so the article can be found by heading navigation."
    },
    "composition": [
      "Content goes inside as children, styled with cardParts: eyebrow, title, body and action.",
      "cardParts.action styles a link. A Button can be the action instead, as in the 'One system, every stack' Card, which composes an image, a title and a primary Button."
    ],
    "example": {
      "language": "tsx",
      "code": "<Card variant=\"elevated\" size=\"md\">\n  <p className={cardParts.eyebrow}>Design system</p>\n  <h3 className={cardParts.title}>Tokens, not hex codes</h3>\n  <p className={cardParts.body}>Components read semantic tokens.</p>\n  <a href=\"/system\" className={cardParts.action}>Read the docs</a>\n</Card>"
    }
  },
  "tokens": [
    {
      "path": "color.surface",
      "type": "color",
      "value": "#1B1C21",
      "description": "Card background",
      "css": "var(--ds-color-surface)",
      "swift": "Tokens.colorSurface",
      "kotlin": "Tokens.colorSurface",
      "usage": "elevated background, and the base of the filled tint."
    },
    {
      "path": "color.text",
      "type": "color",
      "value": "#F2F2F4",
      "description": "Primary text on surface",
      "css": "var(--ds-color-text)",
      "swift": "Tokens.colorText",
      "kotlin": "Tokens.colorText",
      "usage": "Text colour."
    },
    {
      "path": "color.muted",
      "type": "color",
      "value": "#A0A0AB",
      "description": "Secondary text: card body, hints, placeholders",
      "css": "var(--ds-color-muted)",
      "swift": "Tokens.colorMuted",
      "kotlin": "Tokens.colorMuted",
      "usage": "cardParts.body text."
    },
    {
      "path": "color.primary",
      "type": "color",
      "value": "#E23A0D",
      "description": "Brand accent: buttons, links, focus",
      "css": "var(--ds-color-primary)",
      "swift": "Tokens.colorPrimary",
      "kotlin": "Tokens.colorPrimary",
      "usage": "Eyebrow and action text, outlined hover border, filled tint, action focus ring."
    },
    {
      "path": "color.border",
      "type": "color",
      "value": "#2E3037",
      "description": "Hairlines and outlined variants",
      "css": "var(--ds-color-border)",
      "swift": "Tokens.colorBorder",
      "kotlin": "Tokens.colorBorder",
      "usage": "outlined border."
    },
    {
      "path": "radius.card",
      "type": "dimension",
      "value": "0.5rem",
      "css": "var(--ds-radius-card)",
      "swift": "Tokens.radiusCard",
      "kotlin": "Tokens.radiusCard",
      "usage": "Corner radius; the action's focus ring uses half of it."
    },
    {
      "path": "space.sm",
      "type": "dimension",
      "value": "1rem",
      "css": "var(--ds-space-sm)",
      "swift": "Tokens.spaceSm",
      "kotlin": "Tokens.spaceSm",
      "usage": "Padding at size sm (the web recipe's p-4 has the same value)."
    },
    {
      "path": "space.md",
      "type": "dimension",
      "value": "1.25rem",
      "css": "var(--ds-space-md)",
      "swift": "Tokens.spaceMd",
      "kotlin": "Tokens.spaceMd",
      "usage": "Padding at size md (p-5, same value)."
    },
    {
      "path": "space.lg",
      "type": "dimension",
      "value": "1.5rem",
      "css": "var(--ds-space-lg)",
      "swift": "Tokens.spaceLg",
      "kotlin": "Tokens.spaceLg",
      "usage": "Padding at size lg (p-6, same value)."
    },
    {
      "path": "fontSize.title",
      "type": "fontSize",
      "value": "1rem",
      "css": "var(--ds-font-size-title)",
      "swift": "Tokens.fontSizeTitle",
      "kotlin": "Tokens.fontSizeTitle",
      "usage": "cardParts.title text (text-base, same value)."
    },
    {
      "path": "fontSize.body",
      "type": "fontSize",
      "value": "0.875rem",
      "css": "var(--ds-font-size-body)",
      "swift": "Tokens.fontSizeBody",
      "kotlin": "Tokens.fontSizeBody",
      "usage": "cardParts.body and cardParts.action text (text-sm, same value)."
    }
  ],
  "code": {
    "stack": "angular",
    "files": [
      {
        "name": "card.component.ts",
        "language": "ts",
        "code": "import { Component, Input, ViewEncapsulation, booleanAttribute } from \"@angular/core\"\n\n// Card: A surface that groups related content, such as a title, a short text and one action.\n// API from specs/card.spec.json. Styles: card.css, which reads tokens.css\n// (generated, loaded once globally).\n\nexport type CardVariant = \"elevated\" | \"outlined\" | \"filled\"\nexport type CardSize = \"sm\" | \"md\" | \"lg\"\n\n// Content goes inside, styled with the ds-card__eyebrow, __title, __body and\n// __action classes. Encapsulation is off so those classes reach it.\n@Component({\n  selector: \"ds-card\",\n  standalone: true,\n  template: `\n    <article\n      class=\"ds-card\"\n      [attr.data-variant]=\"variant\"\n      [attr.data-size]=\"size\"\n      [attr.aria-disabled]=\"disabled || null\"\n    >\n      <ng-content />\n    </article>\n  `,\n  styleUrl: \"./card.css\",\n  encapsulation: ViewEncapsulation.None,\n})\nexport class CardComponent {\n  @Input() variant: CardVariant = \"elevated\"\n  @Input() size: CardSize = \"md\"\n  @Input({ transform: booleanAttribute }) disabled = false\n}\n"
      },
      {
        "name": "card.css",
        "language": "css",
        "code": "/* Card styles. Reads the CSS variables from tokens.css (generated). */\n\n.ds-card {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: var(--ds-space-sm);\n  width: 100%;\n  border-radius: var(--ds-radius-card);\n  color: var(--ds-color-text);\n  transition: box-shadow 200ms, border-color 200ms, background-color 200ms;\n}\n\n.ds-card[data-size=\"sm\"] {\n  max-width: 15rem;\n  padding: var(--ds-space-sm);\n}\n\n.ds-card[data-size=\"md\"] {\n  max-width: 18rem;\n  padding: var(--ds-space-md);\n}\n\n.ds-card[data-size=\"lg\"] {\n  max-width: 20rem;\n  padding: var(--ds-space-lg);\n}\n\n.ds-card[data-variant=\"elevated\"] {\n  background: var(--ds-color-surface);\n  box-shadow: 0 8px 24px -12px rgb(0 0 0 / 0.35);\n}\n\n.ds-card[data-variant=\"elevated\"]:hover {\n  box-shadow: 0 18px 36px -14px rgb(0 0 0 / 0.5);\n}\n\n.ds-card[data-variant=\"outlined\"] {\n  border: 1px solid var(--ds-color-border);\n  background: transparent;\n}\n\n.ds-card[data-variant=\"outlined\"]:hover {\n  border-color: var(--ds-color-primary);\n}\n\n.ds-card[data-variant=\"filled\"] {\n  background: color-mix(in srgb, var(--ds-color-primary) 10%, var(--ds-color-surface));\n}\n\n.ds-card[data-variant=\"filled\"]:hover {\n  background: color-mix(in srgb, var(--ds-color-primary) 16%, var(--ds-color-surface));\n}\n\n.ds-card[aria-disabled=\"true\"] {\n  opacity: 0.5;\n  pointer-events: none;\n}\n\n.ds-card__eyebrow {\n  margin: 0;\n  font-size: 0.6875rem;\n  font-weight: 500;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  color: var(--ds-color-primary);\n}\n\n.ds-card__title {\n  margin: 0;\n  font-size: var(--ds-font-size-title);\n  font-weight: 600;\n  line-height: 1.375;\n}\n\n.ds-card__body {\n  margin: 0;\n  font-size: var(--ds-font-size-body);\n  line-height: 1.625;\n  color: var(--ds-color-muted);\n}\n\n.ds-card__action {\n  border-radius: calc(var(--ds-radius-card) / 2);\n  font-size: var(--ds-font-size-body);\n  font-weight: 500;\n  color: var(--ds-color-primary);\n  text-underline-offset: 4px;\n}\n\n.ds-card__action:hover {\n  text-decoration: underline;\n}\n\n.ds-card__action:focus-visible {\n  outline: 2px solid var(--ds-color-primary);\n  outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ds-card {\n    transition: none;\n  }\n}\n"
      }
    ],
    "tokenFile": {
      "name": "tokens.css",
      "language": "css",
      "code": "/**\n * Do not edit directly, this file was auto-generated.\n */\n\n:root {\n  --ds-color-surface: #1b1c21; /** Card background */\n  --ds-color-text: #f2f2f4; /** Primary text on surface */\n  --ds-color-muted: #a0a0ab; /** Secondary text: card body, hints, placeholders */\n  --ds-color-primary: #e23a0d; /** Brand accent: buttons, links, focus */\n  --ds-color-on-primary: #111111; /** Text on primary, picked for contrast */\n  --ds-color-border: #2e3037; /** Hairlines and outlined variants */\n  --ds-radius-card: 0.5rem;\n  --ds-radius-button: 0.5rem;\n  --ds-radius-input: 0.5rem;\n  --ds-space-sm: 1rem;\n  --ds-space-md: 1.25rem;\n  --ds-space-lg: 1.5rem;\n  --ds-font-size-title: 1rem;\n  --ds-font-size-body: 0.875rem;\n}"
    },
    "example": {
      "name": "card-example.component.ts",
      "language": "ts",
      "code": "import { Component } from \"@angular/core\"\nimport { ButtonComponent } from \"./button.component\"\nimport { CardComponent } from \"./card.component\"\n\n// The card in the lab: an image, a title and a primary Button as its action.\n@Component({\n  selector: \"app-card-example\",\n  standalone: true,\n  imports: [CardComponent, ButtonComponent],\n  template: `\n    <ds-card variant=\"elevated\" size=\"md\">\n      <img class=\"media\" src=\"/cover.jpg\" alt=\"\" />\n      <h3 class=\"ds-card__title\">Tokens, not hex codes</h3>\n      <button dsButton variant=\"primary\">Read the docs</button>\n    </ds-card>\n  `,\n  styles: `\n    .media {\n      width: 100%;\n      aspect-ratio: 16 / 9;\n      object-fit: cover;\n      border-radius: calc(var(--ds-radius-card) / 2);\n    }\n  `,\n})\nexport class CardExampleComponent {}\n"
    }
  }
}
