{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "membership-highlights",
  "title": "Membership Highlights",
  "description": "Landing-page teaser for a marketplace's memberships. Features up to a set number of plans with name, description, host-formatted price, billing note and their first included features: joinable plans first (recommended leading), each linked to the full memberships page, then announced (coming_soon) plans with a Coming soon label and no Join action. Announced plans that do not fit are counted under the grid, and showComingSoon={false} only counts them. Closed plans are never shown. Renders nothing when no plan is joinable or announced, so a landing page can always mount it. Takes the same MembershipPlan shape as membership-plans; hosts pass their router link through renderLink.",
  "registryDependencies": [
    "badge",
    "card",
    "@agrista/membership-plans"
  ],
  "files": [
    {
      "path": "default/membership-highlights/membership-highlights.tsx",
      "content": "import { useId, type ReactNode } from \"react\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\n\nimport type { MembershipPlan } from \"@/registry/default/membership-plans/membership-plan\"\n\nexport type MembershipHighlightsLinkProps = {\n  href: string\n  className?: string\n  children: ReactNode\n}\n\nexport type MembershipHighlightsProps = {\n  /** Every published membership, as the host already maps it for MembershipPlans. */\n  plans: MembershipPlan[]\n  eyebrow?: string | null\n  title?: string | null\n  description?: string | null\n  /** How many plans to feature. The rest are reached through `href`. */\n  limit?: number\n  /**\n   * Shows announced (`coming_soon`) plans as cards after the joinable ones,\n   * without a Join action. False only counts them under the grid.\n   */\n  showComingSoon?: boolean\n  /** The full memberships page, e.g. /{handle}/join. Omit to hide the see-all link. */\n  href?: string | null\n  /** Where a featured plan's action goes. Defaults to `href`. */\n  planHref?: (plan: MembershipPlan) => string | null | undefined\n  /** How many included features each card lists before it stops. */\n  featureLimit?: number\n  joinLabel?: string\n  /** Badge and footer label on an announced plan's card. */\n  comingSoonBadge?: string\n  seeAllLabel?: string\n  /** The line under the grid for announced plans that did not fit. */\n  comingSoonLabel?: (count: number) => string\n  /** Swaps the plain anchor for the host's router link (next/link, i18n Link). */\n  renderLink?: (props: MembershipHighlightsLinkProps) => ReactNode\n  className?: string\n}\n\nconst primaryLinkClass =\n  \"inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n\nconst comingSoonFooterClass =\n  \"inline-flex h-10 w-full items-center justify-center rounded-md border border-dashed px-4 text-sm font-medium text-muted-foreground\"\n\nconst textLinkClass =\n  \"text-sm font-medium text-primary underline-offset-4 hover:underline\"\n\nfunction defaultLink({ href, className, children }: MembershipHighlightsLinkProps) {\n  return (\n    <a href={href} className={className}>\n      {children}\n    </a>\n  )\n}\n\nfunction isJoinable(plan: MembershipPlan) {\n  return (plan.availability ?? \"available\") === \"available\"\n}\n\nfunction isComingSoon(plan: MembershipPlan) {\n  return plan.availability === \"coming_soon\"\n}\n\n/**\n * Joinable plans first, the recommended one leading and the rest in the order\n * the host published them, then announced plans when `includeComingSoon` is\n * set. Closed (`unavailable`) plans are never featured. Announced plans carry\n * no Join action, so nothing on a landing page is a button the backend would\n * refuse.\n */\nexport function featuredPlans(\n  plans: MembershipPlan[],\n  limit: number,\n  { includeComingSoon = true }: { includeComingSoon?: boolean } = {}\n) {\n  const joinable = plans.filter(isJoinable)\n  const recommended = joinable.filter((plan) => plan.recommended)\n  const others = joinable.filter((plan) => !plan.recommended)\n  const announced = includeComingSoon ? plans.filter(isComingSoon) : []\n\n  return [...recommended, ...others, ...announced].slice(0, Math.max(0, limit))\n}\n\nfunction HighlightCard({\n  plan,\n  href,\n  featureLimit,\n  joinLabel,\n  comingSoonBadge,\n  renderLink,\n}: {\n  plan: MembershipPlan\n  href: string | null | undefined\n  featureLimit: number\n  joinLabel: string\n  comingSoonBadge: string\n  renderLink: (props: MembershipHighlightsLinkProps) => ReactNode\n}) {\n  const comingSoon = isComingSoon(plan)\n  const features = (plan.features ?? [])\n    .filter((feature) => feature.included ?? true)\n    .slice(0, featureLimit)\n  const badge = comingSoon\n    ? comingSoonBadge\n    : plan.badge ?? (plan.recommended ? \"Recommended\" : null)\n\n  return (\n    <Card\n      data-slot=\"membership-highlight\"\n      data-plan-id={plan.id}\n      data-availability={comingSoon ? \"coming_soon\" : \"available\"}\n      className={[\n        \"flex h-full flex-col\",\n        plan.recommended && !comingSoon ? \"border-primary\" : null,\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          {badge ? (\n            <Badge variant={comingSoon ? \"outline\" : \"secondary\"}>{badge}</Badge>\n          ) : null}\n        </div>\n        {plan.description ? (\n          <CardDescription className=\"line-clamp-2\">\n            {plan.description}\n          </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      <CardContent className=\"flex-1\">\n        {features.length > 0 ? (\n          <ul className=\"flex flex-col gap-2 text-sm\">\n            {features.map((feature, index) => (\n              <li\n                key={`${feature.label}-${index}`}\n                className=\"flex items-start gap-2\"\n                data-slot=\"membership-highlight-feature\"\n              >\n                <span aria-hidden=\"true\" className=\"leading-5\">\n                  {\"✓\"}\n                </span>\n                <span className=\"leading-5\">{feature.label}</span>\n              </li>\n            ))}\n          </ul>\n        ) : null}\n      </CardContent>\n\n      {comingSoon ? (\n        <CardFooter>\n          <span\n            data-slot=\"membership-highlight-coming-soon\"\n            className={comingSoonFooterClass}\n          >\n            {comingSoonBadge}\n          </span>\n        </CardFooter>\n      ) : href ? (\n        <CardFooter>\n          {renderLink({\n            href,\n            className: primaryLinkClass,\n            children: (\n              <>\n                {joinLabel}\n                <span className=\"sr-only\"> {plan.name}</span>\n              </>\n            ),\n          })}\n        </CardFooter>\n      ) : null}\n    </Card>\n  )\n}\n\n/**\n * Landing-page teaser for a marketplace's memberships: a heading, up to\n * `limit` plans (joinable first, then announced ones) and a link to the full\n * memberships page. Renders nothing when no plan is joinable or announced, so\n * a page can always mount it.\n */\nexport function MembershipHighlights({\n  plans,\n  eyebrow = \"Membership\",\n  title = \"Grow with a membership\",\n  description,\n  limit = 3,\n  showComingSoon = true,\n  href,\n  planHref,\n  featureLimit = 3,\n  joinLabel = \"Join\",\n  comingSoonBadge = \"Coming soon\",\n  seeAllLabel = \"See all memberships\",\n  comingSoonLabel = (count) =>\n    count === 1 ? \"1 more membership coming soon\" : `${count} more memberships coming soon`,\n  renderLink = defaultLink,\n  className,\n}: MembershipHighlightsProps) {\n  const headingId = useId()\n  const featured = featuredPlans(plans, limit, {\n    includeComingSoon: showComingSoon,\n  })\n  if (featured.length === 0) return null\n\n  const comingSoon =\n    plans.filter(isComingSoon).length - featured.filter(isComingSoon).length\n\n  return (\n    <section\n      data-slot=\"membership-highlights\"\n      aria-labelledby={title ? headingId : undefined}\n      className={[\"flex flex-col gap-8\", className].filter(Boolean).join(\" \")}\n    >\n      <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n        <div className=\"flex max-w-2xl flex-col gap-2\">\n          {eyebrow ? (\n            <p className=\"text-sm font-medium uppercase tracking-wide text-primary\">\n              {eyebrow}\n            </p>\n          ) : null}\n          {title ? (\n            <h2\n              id={headingId}\n              className=\"text-2xl font-semibold tracking-tight sm:text-3xl\"\n            >\n              {title}\n            </h2>\n          ) : null}\n          {description ? (\n            <p className=\"text-muted-foreground\">{description}</p>\n          ) : null}\n        </div>\n        {href\n          ? renderLink({ href, className: textLinkClass, children: seeAllLabel })\n          : null}\n      </div>\n\n      <div\n        className={[\n          \"grid grid-cols-1 gap-6 sm:grid-cols-2\",\n          featured.length >= 3 ? \"lg:grid-cols-3\" : null,\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {featured.map((plan) => (\n          <HighlightCard\n            key={plan.id}\n            plan={plan}\n            href={planHref?.(plan) ?? href}\n            featureLimit={featureLimit}\n            joinLabel={joinLabel}\n            comingSoonBadge={comingSoonBadge}\n            renderLink={renderLink}\n          />\n        ))}\n      </div>\n\n      {comingSoon > 0 ? (\n        <p\n          data-slot=\"membership-highlights-coming-soon\"\n          className=\"text-sm text-muted-foreground\"\n        >\n          {comingSoonLabel(comingSoon)}\n        </p>\n      ) : null}\n    </section>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}