{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "membership-plans",
  "title": "Membership Plans",
  "description": "Presentational section listing the memberships a network admin published on their marketplace. Renders each plan's name, description, host-formatted price, billing note and included/excluded features, and separates what a member can buy now from what is only announced: available plans get a live action, coming_soon and unavailable plans render disabled so they can never be purchased. Marks the member's current plan and switches the other actions to an upgrade/change CTA, so the same section serves both the public marketplace page and the member portal. Hosts wire subscribe/upgrade via onSelect or the action slot.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "separator"
  ],
  "files": [
    {
      "path": "default/membership-plans/membership-plan.ts",
      "content": "import type { ReactNode } from \"react\"\n\n/**\n * Whether a published membership can be bought right now.\n *\n * - `available`   — live on the marketplace, the member can subscribe.\n * - `coming_soon` — the network admin published it for visibility, but it is\n *   not purchasable yet (upstream: a plan flagged for the storefront that is\n *   still in draft).\n * - `unavailable` — shown but closed, e.g. invite-only or a full cohort.\n */\nexport type MembershipAvailability = \"available\" | \"coming_soon\" | \"unavailable\"\n\nexport type MembershipFeature = {\n  label: string\n  /** Defaults to true. False renders the feature as excluded from the plan. */\n  included?: boolean | null\n}\n\nexport type MembershipPlan = {\n  id: string\n  name: string\n  handle?: string | null\n  description?: string | null\n  /**\n   * Preformatted by the host — the registry stays currency- and locale-\n   * agnostic, the same way FarmSaleDetail takes an already-formatted price.\n   */\n  price?: string | null\n  /** Billing cadence or fee note, e.g. \"per month\" or \"+ R500 once-off\". */\n  priceNote?: string | null\n  availability?: MembershipAvailability | null\n  /** Overrides the derived availability badge, e.g. \"Most popular\". */\n  badge?: string | null\n  features?: MembershipFeature[] | null\n  /** Renders the plan as the recommended option (upstream: `is_default`). */\n  recommended?: boolean | null\n}\n\nexport type MembershipPlansProps = {\n  plans: MembershipPlan[]\n  title?: string | null\n  description?: string | null\n  /** Marks one plan as the member's current subscription. */\n  currentPlanId?: string | null\n  /** Called when an available plan's default action is used. */\n  onSelect?: (plan: MembershipPlan) => void\n  /** Replaces the default action control for a plan. */\n  action?: (plan: MembershipPlan) => ReactNode\n  /** Default action label for an available plan the member is not on. */\n  selectLabel?: string\n  /** Action label when at least one other plan is already subscribed to. */\n  changeLabel?: string\n  currentLabel?: string\n  comingSoonLabel?: string\n  unavailableLabel?: string\n  /** Rendered instead of the grid when no memberships are published. */\n  empty?: ReactNode\n  className?: string\n}\n",
      "type": "registry:component"
    },
    {
      "path": "default/membership-plans/membership-plans.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\n\nimport type {\n  MembershipAvailability,\n  MembershipFeature,\n  MembershipPlan,\n  MembershipPlansProps,\n} from \"@/registry/default/membership-plans/membership-plan\"\n\ntype BadgeVariant = \"default\" | \"secondary\" | \"outline\"\n\nfunction availabilityOf(plan: MembershipPlan): MembershipAvailability {\n  return plan.availability ?? \"available\"\n}\n\nfunction FeatureList({ features }: { features: MembershipFeature[] }) {\n  return (\n    <ul className=\"flex flex-col gap-2 text-sm\">\n      {features.map((feature, index) => {\n        const included = feature.included ?? true\n\n        return (\n          <li\n            key={`${feature.label}-${index}`}\n            className=\"flex items-start gap-2\"\n            data-slot=\"membership-feature\"\n            data-included={included}\n          >\n            <span aria-hidden=\"true\" className=\"leading-5\">\n              {included ? \"\\u2713\" : \"\\u2014\"}\n            </span>\n            <span className=\"sr-only\">\n              {included ? \"Included:\" : \"Not included:\"}\n            </span>\n            <span\n              className={\n                included ? \"leading-5\" : \"leading-5 text-muted-foreground\"\n              }\n            >\n              {feature.label}\n            </span>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nfunction MembershipPlanCard({\n  plan,\n  isCurrent,\n  hasCurrentPlan,\n  action,\n  onSelect,\n  selectLabel,\n  changeLabel,\n  currentLabel,\n  comingSoonLabel,\n  unavailableLabel,\n}: {\n  plan: MembershipPlan\n  isCurrent: boolean\n  hasCurrentPlan: boolean\n  action?: (plan: MembershipPlan) => ReactNode\n  onSelect?: (plan: MembershipPlan) => void\n  selectLabel: string\n  changeLabel: string\n  currentLabel: string\n  comingSoonLabel: string\n  unavailableLabel: string\n}) {\n  const availability = availabilityOf(plan)\n  const isPurchasable = availability === \"available\" && !isCurrent\n  const features = plan.features ?? []\n\n  const statusBadge: { label: string; variant: BadgeVariant } | null = isCurrent\n    ? { label: currentLabel, variant: \"default\" }\n    : availability === \"coming_soon\"\n      ? { label: comingSoonLabel, variant: \"secondary\" }\n      : availability === \"unavailable\"\n        ? { label: unavailableLabel, variant: \"outline\" }\n        : plan.badge\n          ? { label: plan.badge, variant: \"secondary\" }\n          : plan.recommended\n            ? { label: \"Recommended\", variant: \"secondary\" }\n            : null\n\n  const actionLabel = isCurrent\n    ? currentLabel\n    : availability === \"coming_soon\"\n      ? comingSoonLabel\n      : availability === \"unavailable\"\n        ? unavailableLabel\n        : hasCurrentPlan\n          ? changeLabel\n          : selectLabel\n\n  const control = action ? (\n    action(plan)\n  ) : (\n    <Button\n      type=\"button\"\n      className=\"w-full\"\n      disabled={!isPurchasable}\n      aria-disabled={!isPurchasable}\n      onClick={isPurchasable && onSelect ? () => onSelect(plan) : undefined}\n    >\n      {actionLabel}\n    </Button>\n  )\n\n  return (\n    <Card\n      data-slot=\"membership-plan\"\n      data-plan-id={plan.id}\n      data-availability={availability}\n      data-current={isCurrent}\n      className={[\n        \"flex h-full flex-col\",\n        plan.recommended && !isCurrent ? \"border-primary\" : null,\n        isCurrent ? \"border-primary ring-1 ring-primary\" : null,\n        availability === \"available\" ? null : \"opacity-75\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <CardHeader className=\"flex flex-col gap-2\">\n        <div className=\"flex flex-wrap items-center justify-between gap-2\">\n          <CardTitle>{plan.name}</CardTitle>\n          {statusBadge ? (\n            <Badge variant={statusBadge.variant}>{statusBadge.label}</Badge>\n          ) : null}\n        </div>\n        {plan.description ? (\n          <CardDescription>{plan.description}</CardDescription>\n        ) : null}\n        {plan.price ? (\n          <p className=\"flex flex-wrap items-baseline gap-1\">\n            <span className=\"text-2xl font-semibold tracking-tight\">\n              {plan.price}\n            </span>\n            {plan.priceNote ? (\n              <span className=\"text-sm text-muted-foreground\">\n                {plan.priceNote}\n              </span>\n            ) : null}\n          </p>\n        ) : null}\n      </CardHeader>\n\n      {features.length > 0 ? (\n        <>\n          <Separator />\n          <CardContent className=\"flex-1 pt-6\">\n            <FeatureList features={features} />\n          </CardContent>\n        </>\n      ) : (\n        <CardContent className=\"flex-1\" />\n      )}\n\n      <CardFooter>{control}</CardFooter>\n    </Card>\n  )\n}\n\nexport function MembershipPlans({\n  plans,\n  title,\n  description,\n  currentPlanId,\n  onSelect,\n  action,\n  selectLabel = \"Join\",\n  changeLabel = \"Switch to this plan\",\n  currentLabel = \"Current plan\",\n  comingSoonLabel = \"Coming soon\",\n  unavailableLabel = \"Unavailable\",\n  empty,\n  className,\n}: MembershipPlansProps) {\n  // Not cross-checked against `plans`: a member keeps their plan after the\n  // network unpublishes it, and they are still switching rather than joining.\n  const hasCurrentPlan = Boolean(currentPlanId)\n\n  const header =\n    title || description ? (\n      <div className=\"flex flex-col gap-2\">\n        {title ? (\n          <h2 className=\"text-2xl font-semibold tracking-tight\">{title}</h2>\n        ) : null}\n        {description ? (\n          <p className=\"text-muted-foreground\">{description}</p>\n        ) : null}\n      </div>\n    ) : null\n\n  return (\n    <section\n      data-slot=\"membership-plans\"\n      className={[\"flex flex-col gap-6\", className].filter(Boolean).join(\" \")}\n    >\n      {header}\n\n      {plans.length === 0 ? (\n        empty ?? (\n          <p className=\"text-sm text-muted-foreground\">\n            No memberships are published yet.\n          </p>\n        )\n      ) : (\n        <div className=\"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n          {plans.map((plan) => (\n            <MembershipPlanCard\n              key={plan.id}\n              plan={plan}\n              isCurrent={plan.id === currentPlanId}\n              hasCurrentPlan={hasCurrentPlan}\n              action={action}\n              onSelect={onSelect}\n              selectLabel={selectLabel}\n              changeLabel={changeLabel}\n              currentLabel={currentLabel}\n              comingSoonLabel={comingSoonLabel}\n              unavailableLabel={unavailableLabel}\n            />\n          ))}\n        </div>\n      )}\n    </section>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}