{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "card",
  "title": "Card",
  "type": "registry:ui",
  "description": "Bordered container with an opinionated header / content / footer layout. Use it as the wrapper around any self-contained block of content — settings panels, dashboard tiles, list cells.",
  "categories": [
    "layout"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/card/card.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { twMerge } from 'tailwind-merge'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { cardVariants, type CardVariants } from './card.variants'\n\ntype Variant = NonNullable<CardVariants['variant']>\n\n/** React's part class strings verbatim (shared with <uip-section-card>). */\nexport const cardClasses = {\n  header:\n    'grid auto-rows-min grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto] items-start gap-1.5 p-4 has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]',\n  title: 'leading-none font-semibold tracking-tight',\n  description: 'text-muted-foreground text-sm',\n  action: 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',\n  content: 'p-4 pt-0',\n  footer: 'flex items-center p-4 pt-0',\n}\n\n// Host utilities that reach the part by inheritance (font size, line height, weight, tracking, colour).\nconst INHERITED = /^(?:[\\w-]+:)*(?:text-|leading-|font-|tracking-)/\n\n/**\n * React's `cn(defaults, className)` for a shadow part: drops the defaults that\n * tailwind-merge says the consumer's classes on `host` override, so e.g.\n * `<uip-card-title class=\"text-2xl\">` loses `leading-none` exactly like\n * `<CardTitle className=\"text-2xl\">`. Counted as consumer classes:\n * `[&::part(<part>)]:x` (also behind variants, `dark:[&::part(base)]:x`) and\n * plain inheritable text utilities on the host. The consumer's utilities still\n * come from the page's stylesheet (on the host / through `::part`); this only\n * removes the defaults they would otherwise have to fight.\n */\nexport function mergeHostClasses(defaults: string, host: Element, part = 'base'): string {\n  const re = new RegExp(`^((?:[\\\\w-]+:)*)\\\\[&::part\\\\(${part}\\\\)\\\\]:(.+)$`)\n  const consumer: string[] = []\n  for (const c of (host.getAttribute('class') || '').split(/\\s+/)) {\n    const m = c.match(re)\n    if (m) consumer.push(m[1] + m[2])\n    else if (c && !c.includes('::part(') && INHERITED.test(c)) consumer.push(c)\n  }\n  if (!consumer.length) return defaults\n  const kept = new Set(twMerge(defaults, consumer.join(' ')).split(' '))\n  return defaults\n    .split(/\\s+/)\n    .filter((c) => kept.has(c))\n    .join(' ')\n}\n\n/**\n * Shared base for the card parts: tailwind sheet, theme bridge,\n * `data-uipkge` / `data-slot` on the host, and a re-render when the host's\n * `class` changes (see `mergeHostClasses`).\n */\nabstract class CardPart extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n  protected abstract slotName: string\n\n  static get observedAttributes() {\n    return [...super.observedAttributes, 'class']\n  }\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', this.slotName)\n  }\n\n  attributeChangedCallback(name: string, old: string | null, value: string | null) {\n    super.attributeChangedCallback(name, old, value)\n    if (name === 'class') this.requestUpdate()\n  }\n\n  /** The part's React class string merged against the consumer's host classes. */\n  protected classes(defaults: string) {\n    return mergeHostClasses(defaults, this)\n  }\n}\n\n/**\n * <uip-card> + parts — the registry Card as web components.\n *\n * Parts don't reference each other by id, so each React part is its own\n * element: <uip-card-header>, <uip-card-title> (an <h3>),\n * <uip-card-description>, <uip-card-action>, <uip-card-content>,\n * <uip-card-footer>. Class strings are React's verbatim, on an inner element\n * exposed as `part=\"base\"`; class overrides (React's `className`) target it\n * with `::part(base)`, e.g. `class=\"[&::part(base)]:border-primary\"`. Layout\n * utilities on the host (max-w-*, margins, space-y-* for the children) work\n * as-is. Like React's `cn()`, a part drops its own defaults that the consumer's\n * classes override (`mergeHostClasses`): `<uip-card-title class=\"text-2xl\">`\n * renders without `leading-none`.\n *\n * The card host is `display: grid` so its inner box fills the host when a grid\n * or flex parent stretches it (equal-height tiles, as React's stretched card).\n */\nexport class UipCard extends CardPart {\n  static styles = [tailwind, css`:host { display: grid; }`]\n  static properties = { variant: { reflect: true } }\n  protected slotName = 'card'\n  variant: Variant = 'default'\n\n  render() {\n    return html`<div part=\"base\" data-slot=\"card\" class=${this.classes(cn(cardVariants({ variant: this.variant }), 'overflow-hidden'))}>\n      <slot></slot>\n    </div>`\n  }\n}\n\n/**\n * React's `has-data-[slot=card-action]:` can't see slotted children, so the\n * header sets `data-action` when a <uip-card-action> is slotted and repeats the\n * two-column grid as `data-[action]:`.\n */\nexport class UipCardHeader extends CardPart {\n  static properties = { hasAction: { state: true } }\n  protected slotName = 'card-header'\n  private hasAction = false\n\n  private onSlotChange(e: Event) {\n    const slot = e.target as HTMLSlotElement\n    this.hasAction = slot.assignedElements().some((el) => el.getAttribute('data-slot') === 'card-action')\n  }\n\n  render() {\n    return html`<div\n      part=\"base\"\n      data-slot=\"card-header\"\n      ?data-action=${this.hasAction}\n      class=${this.classes(cn(cardClasses.header, 'data-[action]:grid-cols-[minmax(0,1fr)_auto]'))}\n    >\n      <slot @slotchange=${this.onSlotChange}></slot>\n    </div>`\n  }\n}\n\nexport class UipCardTitle extends CardPart {\n  protected slotName = 'card-title'\n  render() {\n    return html`<h3 part=\"base\" data-slot=\"card-title\" class=${this.classes(cardClasses.title)}><slot></slot></h3>`\n  }\n}\n\nexport class UipCardDescription extends CardPart {\n  protected slotName = 'card-description'\n  render() {\n    return html`<p part=\"base\" data-slot=\"card-description\" class=${this.classes(cardClasses.description)}><slot></slot></p>`\n  }\n}\n\n/**\n * The action's grid placement has to be on the grid item. The host is\n * `display: contents`, so its inner div is the header's grid item.\n */\nexport class UipCardAction extends CardPart {\n  static styles = [tailwind, css`:host { display: contents; }`]\n  protected slotName = 'card-action'\n  render() {\n    return html`<div part=\"base\" data-slot=\"card-action\" class=${this.classes(cardClasses.action)}><slot></slot></div>`\n  }\n}\n\nexport class UipCardContent extends CardPart {\n  protected slotName = 'card-content'\n  render() {\n    return html`<div part=\"base\" data-slot=\"card-content\" class=${this.classes(cardClasses.content)}><slot></slot></div>`\n  }\n}\n\nexport class UipCardFooter extends CardPart {\n  protected slotName = 'card-footer'\n  render() {\n    return html`<div part=\"base\" data-slot=\"card-footer\" class=${this.classes(cardClasses.footer)}><slot></slot></div>`\n  }\n}\n\nconst define = (tag: string, cls: CustomElementConstructor) => customElements.get(tag) || customElements.define(tag, cls)\ndefine('uip-card', UipCard)\ndefine('uip-card-header', UipCardHeader)\ndefine('uip-card-title', UipCardTitle)\ndefine('uip-card-description', UipCardDescription)\ndefine('uip-card-action', UipCardAction)\ndefine('uip-card-content', UipCardContent)\ndefine('uip-card-footer', UipCardFooter)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-card': UipCard\n    'uip-card-header': UipCardHeader\n    'uip-card-title': UipCardTitle\n    'uip-card-description': UipCardDescription\n    'uip-card-action': UipCardAction\n    'uip-card-content': UipCardContent\n    'uip-card-footer': UipCardFooter\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/card/card.ts"
    },
    {
      "path": "packages/elements-lit/src/components/card/card.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so `Card.vue` can `import { cardVariants } from './card.variants'`\n * without creating a circular dependency back through the index. The circular\n * form caused intermittent `$setup.cardVariants is not a function` errors\n * during dev SSR when several `<Card>` instances rendered in a v-for before\n * the module graph had fully resolved.\n */\nexport const cardVariants = cva(\n  'bg-card text-card-foreground rounded-xl border shadow-sm transition-colors duration-150',\n  {\n    variants: {\n      variant: {\n        default: 'border-border',\n        elevated: 'border-transparent shadow-md hover:shadow-md',\n        outline: 'border-2',\n        ghost: 'border-transparent shadow-none hover:bg-muted/50',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n)\n\nexport type CardVariants = VariantProps<typeof cardVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/card/card.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
