{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card",
  "title": "Card",
  "description": "Card from the Get IT Done design system.",
  "dependencies": [
    "@radix-ui/react-slot@^1.5.0",
    "class-variance-authority@^0.7.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/heading-tag.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/card.tsx",
      "content": "'use client';\n\nimport { cva } from 'class-variance-authority';\nimport { Slot } from '@radix-ui/react-slot';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\nimport type { HeadingTag } from '@/components/ui/heading-tag';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// True below a Card. Children that pad themselves (Accordion) read it to align with the card padding instead of adding their own inset.\nexport const InsideCardContext = React.createContext(false);\n\n\n// Every resting-state color here is the dedicated `card/*` token family (Design Tokens library,\n// already bound in Figma as `colors/card/background/default`, `colors/card/foreground/default`,\n// `colors/card/foreground/muted`, `borderRadius/card`) — never a raw hex/palette value. Interactive/\n// selected/invalid states reuse the SAME system-wide tokens Button/Chip/Alert/Select already use\n// (`background-muted`, `ring-ring-default`, `primary`, `background-accent`, `error`) rather than\n// inventing new ones, so Card's state language stays one system with its siblings.\n//\n// KNOWN TOKEN GAP (CARD-TOK-01 / CARD-A11Y-02): `--colors-card-background-default`, `--colors-card-\n// foreground-default`, and `--colors-card-foreground-muted` (kette-tokens `dist/theme.css`)\n// are declared once at `:root` and never redefined under `.dark` — Card renders the identical\n// light-mode surface/text colors in dark mode. This is the same root cause already flagged on\n// `general-border-default` (see ALERT-TOK-03 in alert.tsx, the token-gap note in chip.tsx): the\n// token repo hasn't shipped a dark-mode value for this family yet. Needs dark-mode-tuned values\n// from design-tokens-maintainer; nothing to change here once they ship — this file never hardcodes\n// around it.\n//\n// KNOWN TOKEN GAP (CARD-A11Y-01): `border-form-border-default` against `bg-card-background-default`\n// computes to ~1.23:1 in light mode (needs ≥3:1 for a UI-component boundary, WCAG 1.4.11) — the\n// resting Card border is effectively edge-less on a white page. This is a token-value defect\n// (`form-border-default` is too light, or a dedicated `card-border-default` token is needed), not a\n// usage defect — Card already consumes the intended semantic border token rather than hardcoding a\n// color. Needs a darker value (or new token) from design-tokens-maintainer.\nconst cardVariants = cva(\n  [\n    'rounded-surface border border-solid border-border bg-card text-card-foreground shadow-xs transition-colors',\n    // Disabled — Card renders a <div>, not a native form control, so (mirrors Chip/AlertClose)\n    // the dimmed/inert look is driven by `aria-disabled`/`data-disabled` rather than the\n    // (inapplicable) `disabled:` pseudo-class. Independent of `interactive`: a purely informational\n    // Card can still be shown as \"unavailable\".\n    //\n    // CARD-A11Y-03 fix: `opacity-disabled` is not a defined Tailwind utility anywhere in this\n    // design system (no `disabled` key in the opacity scale of kette-tokens' preset, built\n    // theme, or this repo's tailwind.config.cjs — confirmed by direct inspection), so it silently\n    // compiled to no CSS rule and the Disabled story rendered pixel-identical to Default. Using the\n    // real `opacity-50` utility here (same interim fallback already used by Tabs/NavigationMenu in\n    // this repo) until design-tokens-maintainer ships a proper `opacity.disabled` scale entry that\n    // every other `opacity-disabled` caller in this library can also migrate to.\n    'aria-disabled:pointer-events-none aria-disabled:opacity-50',\n    'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',\n    // Invalid — e.g. a form-section Card reporting a validation error. Mirrors\n    // `SelectTrigger`'s `aria-[invalid=true]:border-error` (select.tsx) so the boundary-only\n    // (no background refill) treatment stays one convention across form-adjacent containers.\n    'aria-[invalid=true]:border-error',\n    // Selected — a persistent \"this is the chosen one\" identity for a card among alternatives\n    // (e.g. a plan/tile picker), independent of `interactive`. Mirrors Checkbox's\n    // `data-[state=checked]:border-primary` for the boundary and reuses `background-accent`\n    // (already the selected/highlighted surface tone in Select's option list) for the fill.\n    'data-[selected=true]:border-primary data-[selected=true]:bg-background-accent [&_.text-text-subtle]:data-[selected=true]:text-text-subtle-on-muted [&_.text-card-foreground-muted]:data-[selected=true]:text-text-subtle-on-muted',\n    // Today — the current day among day cards (calendar, agenda). Brand border and subtle brand fill.\n    'data-[today=true]:border-border-brand data-[today=true]:bg-background-brand-subtle',\n  ],\n  {\n    variants: {\n      // Only set when the consumer passes `onClick` (see the `interactive` computation in `Card`\n      // below) — a static content container never grows hover/focus/press affordances it can't\n      // back with real behavior.\n      interactive: {\n        true: [\n          'cursor-pointer',\n          'focus-visible:outline-none focus-visible:ring-focus focus-visible:ring-offset-focus focus-visible:ring-offset-background focus-visible:ring-ring-default focus-visible:border-ring',\n          'aria-[invalid=true]:focus-visible:ring-error',\n        ],\n        false: '',\n      },\n      // `default` keeps the established look. `surface` is the Figma Card (Variant=Surface): hover raises the border and shadow instead of tinting the fill.\n      variant: {\n        default: '',\n        surface: '',\n      },\n    },\n    compoundVariants: [\n      {\n        interactive: true,\n        variant: 'default',\n        className: 'hover:bg-background-neutral-default active:bg-background-neutral-default active:brightness-95',\n      },\n      {\n        interactive: true,\n        variant: 'surface',\n        className: 'hover:border-border-bold hover:shadow-sm active:border-border-bold active:shadow-sm',\n      },\n    ],\n    defaultVariants: {\n      interactive: false,\n      variant: 'default',\n    },\n  },\n);\n\nexport interface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n  // Link mode: renders the card as an anchor with the interactive hover and focus look. No role=\"button\" (a real link).\n  href?: string;\n  target?: string;\n  rel?: string;\n  // Link mode with your own link component (router Link): the single child is the link element and receives the card styles.\n  asChild?: boolean;\n  // Visual preset. Default keeps the established tinted hover; surface raises border and shadow on hover.\n  variant?: 'default' | 'surface';\n  /**\n   * Marks this card as the chosen option among a set of alternatives (e.g. a plan/tile picker).\n   * Independent of `onClick`/`interactive` — a purely informational Card can still indicate\n   * \"this is the current one.\" Sets `data-selected` always, and `aria-pressed` only while\n   * `interactive` (a plain `<div>` has no ARIA widget role to carry `aria-selected`/`aria-pressed`\n   * otherwise — mirrors Chip's identical reasoning, see chip.tsx). On a non-interactive selected\n   * Card, an sr-only \" (selected)\" suffix carries the state to assistive tech instead.\n   */\n  selected?: boolean;\n  /**\n   * Marks the card of the current day. Sets `aria-current=\"date\"` and `data-today`, and (while not interactive) an sr-only \" (today)\" suffix, so it is not announced as \"selected\". Independent of `selected`.\n   */\n  today?: boolean;\n  /**\n   * Dims the card and blocks pointer interaction/keyboard activation. Mirrors Chip's\n   * `aria-disabled`/`data-disabled` approach rather than the HTML `disabled` attribute, since Card\n   * is not a native form control.\n   */\n  disabled?: boolean;\n  /**\n   * Forces the `error` border/focus-ring treatment and sets `aria-invalid` — e.g. a form-section\n   * Card reporting a validation error. An sr-only \" (invalid)\" suffix mirrors Chip's convention so\n   * the state isn't color-only.\n   */\n  invalid?: boolean;\n  /**\n   * CARD-A11Y-04: for an interactive Card (`onClick` set) whose children are rich nested content\n   * (heading + description + image, etc.), `role=\"button\"`'s accessible name otherwise defaults to\n   * the concatenation of ALL descendant text/alt — e.g. \"Category I am a h3 Description button\"\n   * instead of a concise \"Title\". Pass `aria-label` (already part of `React.HTMLAttributes`, so no\n   * new prop is introduced — this JSDoc documents the pattern) with a short, meaningful label, or\n   * wire `aria-labelledby` to the `id` of the `CardTitle`, so\n   * `screen.getByRole('button', { name: '<title>' })` resolves. See the `InteractiveWithAccessibleName`\n   * story.\n   */\n  'aria-label'?: string;\n}\n\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(\n  ({ className, variant, selected, today, disabled, invalid, onClick, onKeyDown, href, asChild = false, children, ...props }, ref) => {\n    // Interactivity is derived from the presence of a real handler (mirrors Chip) rather than a\n    // separate boolean prop, so there is exactly one way to opt a Card into being a keyboard-\n    // operable \"clickable card\" and it can never be turned on without something to activate.\n    const l = useLabels();\n    const zero = usePreset() === 'zero';\n    const isLink = Boolean(href) || asChild;\n    const interactive = (Boolean(onClick) || isLink) && !disabled;\n    // A link keeps its native role, tab stop and Enter activation; only a div-as-button needs them added.\n    const asButton = interactive && !isLink;\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n      // Enter and Space from nested buttons, links or inputs belong to them, not to the card.\n      if (!asButton || event.defaultPrevented || event.target !== event.currentTarget) return;\n      if (event.key === 'Enter' || event.key === ' ') {\n        event.preventDefault();\n        onClick?.(event as unknown as React.MouseEvent<HTMLDivElement>);\n      }\n    };\n\n    // A keyboard press on a nested control makes the browser fire a click that bubbles here: ignore it (detail 0 marks a keyboard or programmatic click).\n    const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {\n      if (asButton && event.detail === 0 && event.target !== event.currentTarget) return;\n      onClick?.(event);\n    };\n\n    const Comp: React.ElementType = asChild ? Slot : href ? 'a' : 'div';\n    const suffixes = (\n      <>\n        {selected && !interactive && <span className=\"sr-only\">{l.selectedSuffix}</span>}\n        {today && !interactive && <span className=\"sr-only\">{l.todaySuffix}</span>}\n        {invalid && <span className=\"sr-only\">{l.invalidSuffix}</span>}\n      </>\n    );\n\n    return (\n      <InsideCardContext.Provider value={true}>\n      <Comp\n        ref={ref}\n        role={asButton ? 'button' : undefined}\n        tabIndex={asButton ? 0 : undefined}\n        href={href && !disabled ? href : undefined}\n        aria-pressed={asButton && selected != null ? Boolean(selected) : undefined}\n        aria-disabled={disabled || undefined}\n        aria-invalid={invalid || undefined}\n        data-disabled={disabled || undefined}\n        data-invalid={invalid || undefined}\n        data-selected={selected || undefined}\n        aria-current={today ? 'date' : isLink && selected ? 'true' : undefined}\n        data-today={today || undefined}\n        onClick={onClick ? handleClick : undefined}\n        onKeyDown={handleKeyDown}\n        className={cn(cardVariants({ interactive, variant }), zero && 'shadow-elevation-surface border-border-default data-[selected=true]:border-border-brand data-[selected=true]:bg-background data-[selected=true]:ring-[3px] data-[selected=true]:ring-background-brand-subtle', isLink && 'block no-underline text-inherit', className)}\n        {...props}\n      >\n        {asChild ? (\n          children\n        ) : (\n          <>\n            {children}\n            {suffixes}\n          </>\n        )}\n      </Comp>\n      </InsideCardContext.Provider>\n    );\n  },\n);\nCard.displayName = 'Card';\n\nexport interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n  // Controls (menu button, badge, buttons) aligned to the end of the header.\n  actions?: React.ReactNode;\n  // Avatar, icon tile or logo before the title block.\n  leading?: React.ReactNode;\n  // 4px brand line along the top edge.\n  accent?: boolean;\n  // Hairline under the header.\n  divider?: boolean;\n}\n\nconst CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(\n  ({ className, actions, leading, accent, divider, children, ...props }, ref) => {\n    const zero = usePreset() === 'zero';\n    const pad = zero ? 'px-5 pt-4 pb-3' : 'p-6';\n    const decorations = cn(\n      accent && 'rounded-t-surface border-t-4 border-solid border-border-brand',\n      divider && cn('border-b border-solid border-border', zero ? 'pb-3' : 'pb-6'),\n    );\n    if (!actions && !leading) {\n      return (\n        <div ref={ref} className={cn('flex flex-col space-y-[6px]', pad, decorations, className)} {...props}>\n          {children}\n        </div>\n      );\n    }\n    return (\n      <div ref={ref} className={cn('flex items-start gap-3', pad, decorations, className)} {...props}>\n        {leading ? <div className=\"shrink-0\">{leading}</div> : null}\n        <div className=\"flex min-w-0 flex-1 flex-col space-y-[6px]\">{children}</div>\n        {actions ? <div className=\"flex shrink-0 items-center gap-2\">{actions}</div> : null}\n      </div>\n    );\n  },\n);\nCardHeader.displayName = 'CardHeader';\n\nexport interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {\n  // Heading element, so the card fits the document outline. Default h3.\n  titleAs?: HeadingTag;\n}\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, CardTitleProps>(\n  ({ className, titleAs: Title = 'h3', ...props }, ref) => {\n    const zero = usePreset() === 'zero';\n    return (\n      // The rule can't see through the `{...props}` spread; every consumer passes visible title text\n      // as children (see stories/tests) — same pre-existing false positive already suppressed on\n      // AlertTitle (alert.tsx).\n      // eslint-disable-next-line jsx-a11y/heading-has-content\n      <Title ref={ref} className={cn(zero ? 'typography-ui-lg-semibold tracking-[-0.01em]' : 'font-semibold leading-none tracking-[-0.01em]', 'text-card-foreground', className)} {...props} />\n    );\n  },\n);\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => (\n    // `text-card-foreground-muted` is the dedicated token (Figma `colors/card/foreground/muted`,\n    // already shipped as `--colors-card-foreground-muted`) — swapped in for the previous\n    // `text-muted-foreground`, which this design system never defines as a utility (same\n    // undefined-class issue already fixed on Table's empty-state caption, see table.tsx) and so\n    // silently rendered with no color at all.\n    <p ref={ref} className={cn('text-sm text-card-foreground-muted [[data-selected=true]_&]:text-text-default [[data-today=true]_&]:text-text-default', className)} {...props} />\n  ),\n);\nCardDescription.displayName = 'CardDescription';\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const zero = usePreset() === 'zero';\n    return <div ref={ref} className={cn(zero ? 'px-5 pb-5 pt-0' : 'p-6 pt-0', '[&>p]:last-of-type:mb-0', className)} {...props} />;\n  },\n);\nCardContent.displayName = 'CardContent';\n\nexport interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n  // Action row style: hairline above, padded to the card edge (\"View report\" link or buttons).\n  divider?: boolean;\n  align?: 'start' | 'end' | 'between';\n}\n\nconst footerAlign = { start: 'justify-start', end: 'justify-end', between: 'justify-between' } as const;\n\nconst CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(({ className, divider, align, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  return (\n  <div\n    ref={ref}\n    className={cn(\n      'flex items-center',\n      zero ? 'px-5 pb-5' : 'p-6',\n      'pt-0',\n      divider && 'gap-4 border-t border-solid border-border pt-4',\n      align && footerAlign[align],\n      className,\n    )}\n    {...props}\n  />\n  );\n});\nCardFooter.displayName = 'CardFooter';\n\nexport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };\n\n\n// --- Deliberately NOT added (documented rather than silently omitted) ---\n//\n// A Figma Code Connect mapping (`card.figma.tsx`): unlike Chip/Alert (which each have a genuine,\n// name-matching `component_set` indexed in the published \"🚧 GetITDone Basic\" library, just\n// orphaned from the live canvas — see chip.figma.tsx / alert.figma.tsx), this generic composable\n// Card (Card/CardHeader/CardTitle/CardDescription/CardContent/CardFooter) has no matching Figma\n// component at all, live or orphaned. `search_design_system` only turns up unrelated marketing\n// composites (\"Blog post card\", \"_Pricing tier card\", \"Credit card mockup\", …) with entirely\n// different anatomy/props — mapping this Card to one of those would misrepresent the design system,\n// so no `.figma.tsx` was fabricated. The only genuine, live Figma spec found for this component is\n// the token layer consumed above (`colors/card/background/default`, `colors/card/foreground/\n// default`, `colors/card/foreground/muted`, `borderRadius/card`).\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}