{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "registration-card",
  "title": "Registration Card",
  "author": "ilinxa",
  "description": "Event registration status card — capacity progress, spots-left counter, status-aware call to action, and a share slot.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "progress",
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/data/registration-card/registration-card.tsx",
      "content": "\"use client\";\n\nimport { memo, useId, useMemo } from \"react\";\nimport { Share2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  DEFAULT_REGISTRATION_CARD_LABELS,\n  type RegistrationCardLabels,\n  type RegistrationCardProps,\n  type RegistrationStatus,\n} from \"./types\";\nimport { deriveRegistrationStatus } from \"./lib/registration-status\";\nimport { defaultFormatSpotsLeftSuffix } from \"./lib/format-default\";\n\nfunction getCtaConfig(\n  status: RegistrationStatus,\n  hasOnRegister: boolean,\n  labels: Required<RegistrationCardLabels>,\n): {\n  label: string;\n  variant: \"default\" | \"secondary\";\n  disabled: boolean;\n} {\n  if (!hasOnRegister) {\n    return {\n      label: labels.ctaUnavailable,\n      variant: \"secondary\",\n      disabled: true,\n    };\n  }\n  if (status === \"full\") {\n    return { label: labels.ctaSoldOut, variant: \"secondary\", disabled: true };\n  }\n  if (status === \"closed\") {\n    return { label: labels.ctaClosed, variant: \"secondary\", disabled: true };\n  }\n  return { label: labels.ctaRegister, variant: \"default\", disabled: false };\n}\n\nfunction RegistrationCardInner({\n  capacity,\n  registered,\n  closed = false,\n  statusOverride,\n  lastSpotsRatio = 0.8,\n  urgentSpotsCount = 10,\n  formatSpotsLeftSuffix,\n  heading,\n  headingAs = \"h3\",\n  framed = true,\n  onRegister,\n  onShare,\n  actions,\n  labels: labelsProp,\n  className,\n  headingClassName,\n  barClassName,\n  ctaClassName,\n}: RegistrationCardProps) {\n  const headingId = useId();\n  const HeadingTag = headingAs;\n\n  const labels = useMemo<Required<RegistrationCardLabels>>(\n    () => ({ ...DEFAULT_REGISTRATION_CARD_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  const spotsLeftSuffixFormatter = useMemo<(count: number) => string>(() => {\n    if (formatSpotsLeftSuffix) return formatSpotsLeftSuffix;\n    if (labelsProp?.spotsLeftSuffix !== undefined) {\n      const suffix = labelsProp.spotsLeftSuffix;\n      return () => suffix;\n    }\n    return defaultFormatSpotsLeftSuffix;\n  }, [formatSpotsLeftSuffix, labelsProp]);\n\n  const status = useMemo<RegistrationStatus>(\n    () =>\n      statusOverride ??\n      deriveRegistrationStatus({\n        capacity,\n        registered,\n        closed,\n        lastSpotsRatio,\n      }),\n    [statusOverride, capacity, registered, closed, lastSpotsRatio],\n  );\n\n  const hasQuota = capacity != null && registered != null;\n  const spotsLeft = hasQuota ? Math.max(0, capacity - registered) : 0;\n  const percent = hasQuota\n    ? capacity === 0\n      ? 100\n      : Math.min(100, (registered / capacity) * 100)\n    : 0;\n  const isUrgent = hasQuota && spotsLeft > 0 && spotsLeft <= urgentSpotsCount;\n\n  const cta = getCtaConfig(status, onRegister != null, labels);\n\n  return (\n    <section\n      aria-labelledby={heading ? headingId : undefined}\n      className={cn(\n        framed &&\n          \"bg-card rounded-2xl p-6 border border-border/50 shadow-lg\",\n        className,\n      )}\n    >\n      {heading && (\n        <HeadingTag\n          id={headingId}\n          className={cn(\n            \"text-lg font-semibold text-foreground mb-4\",\n            headingClassName,\n          )}\n        >\n          {heading}\n        </HeadingTag>\n      )}\n\n      {hasQuota && (\n        <div className={cn(\"mb-6\", barClassName)}>\n          <div className=\"flex justify-between text-sm mb-2\">\n            <span className=\"text-muted-foreground\">{labels.capacityLabel}</span>\n            <span\n              className={cn(\n                \"font-medium\",\n                isUrgent ? \"text-destructive\" : \"text-foreground\",\n              )}\n            >\n              {spotsLeft > 0\n                ? `${spotsLeft} ${spotsLeftSuffixFormatter(spotsLeft)}`\n                : labels.spotsLeftFull}\n            </span>\n          </div>\n          <Progress\n            value={percent}\n            className=\"h-3\"\n            aria-label={labels.ariaLabel}\n          />\n          <div className=\"flex justify-between text-xs text-muted-foreground mt-1\">\n            <span>\n              {registered} {labels.registeredSuffix}\n            </span>\n            <span>\n              {capacity} {labels.capacitySuffix}\n            </span>\n          </div>\n        </div>\n      )}\n\n      <Button\n        type=\"button\"\n        className={cn(\"w-full\", ctaClassName)}\n        size=\"lg\"\n        variant={cta.variant}\n        disabled={cta.disabled}\n        onClick={cta.disabled ? undefined : onRegister}\n      >\n        {cta.label}\n      </Button>\n\n      {actions ? (\n        <div className=\"mt-3\">{actions}</div>\n      ) : onShare ? (\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          className=\"w-full mt-3 gap-2\"\n          onClick={onShare}\n        >\n          <Share2 aria-hidden=\"true\" className=\"w-4 h-4\" />\n          {labels.ctaShare}\n        </Button>\n      ) : null}\n    </section>\n  );\n}\n\nconst RegistrationCard = memo(RegistrationCardInner);\nRegistrationCard.displayName = \"RegistrationCard\";\n\nexport { RegistrationCard };\nexport default RegistrationCard;\n",
      "type": "registry:component",
      "target": "components/registration-card/registration-card.tsx"
    },
    {
      "path": "src/registry/components/data/registration-card/index.ts",
      "content": "export { RegistrationCard, default } from \"./registration-card\";\n\nexport type {\n  RegistrationCardProps,\n  RegistrationCardLabels,\n  RegistrationStatus,\n} from \"./types\";\n\nexport { DEFAULT_REGISTRATION_CARD_LABELS } from \"./types\";\n\nexport {\n  deriveRegistrationStatus,\n  type DeriveRegistrationStatusOptions,\n} from \"./lib/registration-status\";\n\n",
      "type": "registry:component",
      "target": "components/registration-card/index.ts"
    },
    {
      "path": "src/registry/components/data/registration-card/types.ts",
      "content": "import type { ReactNode } from \"react\";\nimport type { RegistrationStatus } from \"./lib/registration-status\";\n\nexport type { RegistrationStatus };\n\nexport interface RegistrationCardLabels {\n  /** Default: \"Capacity\". Top-row left label above the bar. */\n  capacityLabel?: ReactNode;\n  /**\n   * Default: \"spots left\". Counter suffix when spots remain.\n   * @deprecated Use `formatSpotsLeftSuffix` (callback) for plural-correct labels.\n   *   Setting this still works (used as a static suffix for all counts) but\n   *   produces ungrammatical output at count === 1 (\"1 spots left\").\n   */\n  spotsLeftSuffix?: string;\n  /** Default: \"Sold out\". Counter text when capacity reached. */\n  spotsLeftFull?: string;\n  /** Default: \"registered\". Bottom-row left counter suffix. */\n  registeredSuffix?: string;\n  /** Default: \"capacity\". Bottom-row right counter suffix. */\n  capacitySuffix?: string;\n  /** Default: \"Register\". Primary CTA when registration is open. */\n  ctaRegister?: string;\n  /** Default: \"Sold out\". Primary CTA when status === 'full'. */\n  ctaSoldOut?: string;\n  /** Default: \"Closed\". Primary CTA when status === 'closed'. */\n  ctaClosed?: string;\n  /** Default: \"Registration unavailable\". Primary CTA when no onRegister provided. */\n  ctaUnavailable?: string;\n  /** Default: \"Share\". Secondary share button label (when default share is rendered). */\n  ctaShare?: string;\n  /** aria-label on the progress bar. Default: \"Registration capacity\". */\n  ariaLabel?: string;\n}\n\nexport interface RegistrationCardProps {\n  /** Total seats / units. Optional — when absent, no-quota mode (bar hidden). */\n  capacity?: number;\n  /** Currently registered count. Optional — when absent, no-quota mode. */\n  registered?: number;\n  /** Explicitly close registration regardless of capacity. Default: false. */\n  closed?: boolean;\n\n  /** Override the auto-derived state. Rare — for preview / what-if. */\n  statusOverride?: RegistrationStatus;\n\n  // ─── Thresholds ─────────────────────────────────────────────────\n  /** Percent (0–1) of capacity at which status flips to 'lastSpots'. Default: 0.8. */\n  lastSpotsRatio?: number;\n  /** Absolute spots-left count at which the counter color flips to text-destructive. Default: 10. */\n  urgentSpotsCount?: number;\n\n  // ─── Formatters ──────────────────────────────────────────────────\n  /**\n   * Format the spots-left suffix as a function of count. Default uses\n   * `Intl.PluralRules` (\"spot left\" at 1, \"spots left\" otherwise). Receives\n   * the integer spots-left count, returns a string. Takes precedence over\n   * `labels.spotsLeftSuffix`.\n   */\n  formatSpotsLeftSuffix?: (count: number) => string;\n\n  // ─── Heading ─────────────────────────────────────────────────────\n  /** Optional section heading. */\n  heading?: string;\n  /** Heading semantic level. Default: 'h3'. */\n  headingAs?: \"h2\" | \"h3\" | \"h4\";\n\n  // ─── Visual ──────────────────────────────────────────────────────\n  /** Wrap in card chrome (`bg-card rounded-2xl p-6 border shadow-lg`). Default: true. */\n  framed?: boolean;\n\n  // ─── Actions ─────────────────────────────────────────────────────\n  /** Primary CTA click handler. Optional — when omitted, CTA renders as disabled with `ctaUnavailable` label. */\n  onRegister?: () => void;\n  /** Default share-button click handler. When provided + `actions` NOT provided, renders an outline Share button. */\n  onShare?: () => void;\n  /** Custom action(s) slot — fully replaces the default share button. */\n  actions?: ReactNode;\n\n  /** Localized labels. */\n  labels?: RegistrationCardLabels;\n\n  // ─── Style overrides ─────────────────────────────────────────────\n  className?: string;\n  headingClassName?: string;\n  barClassName?: string;\n  ctaClassName?: string;\n}\n\n/** Default English labels — exported for consumer composition. */\nexport const DEFAULT_REGISTRATION_CARD_LABELS: Required<RegistrationCardLabels> = {\n  capacityLabel: \"Capacity\",\n  spotsLeftSuffix: \"spots left\",\n  spotsLeftFull: \"Sold out\",\n  registeredSuffix: \"registered\",\n  capacitySuffix: \"capacity\",\n  ctaRegister: \"Register\",\n  ctaSoldOut: \"Sold out\",\n  ctaClosed: \"Closed\",\n  ctaUnavailable: \"Registration unavailable\",\n  ctaShare: \"Share\",\n  ariaLabel: \"Registration capacity\",\n};\n",
      "type": "registry:component",
      "target": "components/registration-card/types.ts"
    },
    {
      "path": "src/registry/components/data/registration-card/lib/registration-status.ts",
      "content": "export type RegistrationStatus = \"open\" | \"lastSpots\" | \"full\" | \"closed\";\n\nexport interface DeriveRegistrationStatusOptions {\n  capacity?: number;\n  registered?: number;\n  /** Explicitly close registration regardless of capacity. */\n  closed?: boolean;\n  /** Percent (0–1) of capacity at which status flips to 'lastSpots'. Default: 0.8. */\n  lastSpotsRatio?: number;\n}\n\n/**\n * Pure function. Derives registration status from capacity + registered + closed flag.\n *\n * - `closed: true` always returns \"closed\"\n * - capacity / registered missing → \"open\" (no-quota mode)\n * - registered >= capacity → \"full\"\n * - registered/capacity >= lastSpotsRatio → \"lastSpots\"\n * - otherwise → \"open\"\n */\nexport function deriveRegistrationStatus(\n  opts: DeriveRegistrationStatusOptions,\n): RegistrationStatus {\n  if (opts.closed === true) return \"closed\";\n\n  const { capacity, registered } = opts;\n  if (capacity == null || registered == null) return \"open\";\n\n  if (registered >= capacity) return \"full\";\n\n  const ratio = capacity === 0 ? 1 : registered / capacity;\n  const threshold = opts.lastSpotsRatio ?? 0.8;\n  if (ratio >= threshold) return \"lastSpots\";\n\n  return \"open\";\n}\n",
      "type": "registry:component",
      "target": "components/registration-card/lib/registration-status.ts"
    },
    {
      "path": "src/registry/components/data/registration-card/lib/format-default.ts",
      "content": "const enPluralRules = new Intl.PluralRules(\"en\");\n\n/** Default English plural-correct spots-left suffix. \"spot left\" / \"spots left\". */\nexport function defaultFormatSpotsLeftSuffix(count: number): string {\n  return enPluralRules.select(count) === \"one\" ? \"spot left\" : \"spots left\";\n}\n",
      "type": "registry:component",
      "target": "components/registration-card/lib/format-default.ts"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:block"
}