{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-promo",
  "title": "Feature Promo",
  "author": "@fma-ds/ds",
  "description": "A locked feature or an offer on a card: why it matters, what it gives and the call to action.\n\nInside `FeaturePromo`, put:\n\n- **`FeaturePromoContent`**: with `FeaturePromoTitle`, one or more `FeaturePromoDescription`, `FeaturePromoBenefits` of `FeaturePromoBenefit` (with an `icon` tile) and `FeaturePromoActions`.\n- **`FeaturePromoMedia`** (optional): a framed panel — no background images or gradients.\n- **`FeaturePromoGrid`** (optional): spans the full width with one to three columns — choice cards, `Item`s with `href` for shortcuts, or cards that explain options.\n\n`variant=\"brand\"` frames the card with a primary → brand gradient border.\n\n`layout=\"split\"` puts the media beside the content and hides it when the card is narrow; `layout=\"centered\"` stacks everything in the middle.\n\nWith a `FeaturePromoAcceptance` (a checkbox card), the buttons in `FeaturePromoActions` stay disabled until it is checked — uncontrolled (`defaultAccepted`) or controlled (`isAccepted` + `onAcceptedChange`).",
  "dependencies": [
    "cn@^0.5.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/checkbox",
    "@fma-ds/icon-tile",
    "@fma-ds/brand"
  ],
  "files": [
    {
      "path": "src/blocks/feature-promo/feature-promo.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport { ButtonContext, type CheckboxProps, LinkContext } from \"react-aria-components\";\nimport { CheckboxCard } from \"@/core/checkbox/checkbox\";\nimport { IconTile } from \"@/core/icon-tile/icon-tile\";\nimport { brandGradientBorder } from \"@/lib/brand\";\n\ntype FeaturePromoContextValue = {\n  isAccepted: boolean;\n  setAccepted: (isAccepted: boolean) => void;\n  hasAcceptance: boolean;\n  registerAcceptance: () => () => void;\n};\n\nconst FeaturePromoContext = React.createContext<FeaturePromoContextValue | null>(null);\n\nfunction useFeaturePromo(part: string) {\n  const context = React.useContext(FeaturePromoContext);\n  if (!context) throw new Error(`${part} must be used within FeaturePromo`);\n  return context;\n}\n\n/**\n * A locked feature or an offer on a card surface. `split` puts the content on\n * the left and the `FeaturePromoMedia` on the right (hidden when the card is\n * narrow); `centered` stacks everything in the middle. A `FeaturePromoGrid`\n * spans the full width below the content in both.\n *\n * With a `FeaturePromoAcceptance` inside, the buttons in `FeaturePromoActions`\n * stay disabled until it's checked; the state is uncontrolled\n * (`defaultAccepted`) or controlled (`isAccepted` + `onAcceptedChange`).\n */\nfunction FeaturePromo({\n  layout = \"split\",\n  variant = \"default\",\n  isAccepted: isAcceptedProp,\n  defaultAccepted = false,\n  onAcceptedChange,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"section\"> & {\n  layout?: \"split\" | \"centered\";\n  /** `brand` frames the card with a primary → brand gradient border. */\n  variant?: \"default\" | \"brand\";\n  isAccepted?: boolean;\n  defaultAccepted?: boolean;\n  onAcceptedChange?: (isAccepted: boolean) => void;\n}) {\n  const [uncontrolledAccepted, setUncontrolledAccepted] = React.useState(defaultAccepted);\n  const isAccepted = isAcceptedProp ?? uncontrolledAccepted;\n  const [acceptanceCount, setAcceptanceCount] = React.useState(0);\n  const registerAcceptance = React.useCallback(() => {\n    setAcceptanceCount((count) => count + 1);\n    return () => setAcceptanceCount((count) => count - 1);\n  }, []);\n\n  const context = React.useMemo<FeaturePromoContextValue>(\n    () => ({\n      isAccepted,\n      setAccepted: (next) => {\n        setUncontrolledAccepted(next);\n        onAcceptedChange?.(next);\n      },\n      hasAcceptance: acceptanceCount > 0,\n      registerAcceptance,\n    }),\n    [isAccepted, onAcceptedChange, acceptanceCount, registerAcceptance],\n  );\n\n  return (\n    <FeaturePromoContext.Provider value={context}>\n      <section\n        data-slot=\"feature-promo\"\n        data-layout={layout}\n        data-variant={variant}\n        className={cn(\n          \"group/feature-promo @container w-full overflow-hidden rounded-xl p-6 text-sm text-card-foreground\",\n          variant === \"brand\" ? brandGradientBorder : \"bg-card ring-1 ring-foreground/10\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"grid gap-8 group-data-[layout=split]/feature-promo:@3xl:grid-cols-[minmax(0,36rem)_minmax(0,1fr)]\">\n          {children}\n        </div>\n      </section>\n    </FeaturePromoContext.Provider>\n  );\n}\n\n/**\n * Wraps the content parts. Put it, the media and the grid directly inside\n * `FeaturePromo`; the layout places them.\n */\nfunction FeaturePromoContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"feature-promo-content\"\n      className={cn(\n        \"flex flex-col gap-4\",\n        \"group-data-[layout=centered]/feature-promo:mx-auto group-data-[layout=centered]/feature-promo:max-w-2xl group-data-[layout=centered]/feature-promo:items-center group-data-[layout=centered]/feature-promo:text-center\",\n        \"[&_[data-slot=feature-promo-acceptance]]:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** The promo's heading, an `h2` (the page header holds the `h1`). */\nfunction FeaturePromoTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  return (\n    <h2\n      data-slot=\"feature-promo-title\"\n      className={cn(\n        \"font-heading text-xl leading-snug font-semibold text-balance @3xl:text-2xl\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A paragraph of supporting text; use several for longer copy. */\nfunction FeaturePromoDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"feature-promo-description\"\n      className={cn(\"text-base text-pretty text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The list of what the feature gives, one `FeaturePromoBenefit` per item. */\nfunction FeaturePromoBenefits({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"feature-promo-benefits\"\n      className={cn(\n        \"flex flex-col gap-3 py-1 text-left group-data-[layout=centered]/feature-promo:w-fit\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** One benefit: the `icon` in a tinted round tile, then the text. */\nfunction FeaturePromoBenefit({\n  icon,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"li\"> & { icon: React.ReactNode }) {\n  return (\n    <li\n      data-slot=\"feature-promo-benefit\"\n      className={cn(\"flex items-center gap-3 text-base font-medium\", className)}\n      {...props}\n    >\n      <IconTile variant=\"soft\" radius=\"full\" aria-hidden=\"true\">\n        {icon}\n      </IconTile>\n      <span>{children}</span>\n    </li>\n  );\n}\n\n/**\n * A checkbox card the user must check before the actions unlock (\"I\n * understand…\"). Its state lives on `FeaturePromo`.\n */\nfunction FeaturePromoAcceptance(\n  props: Omit<CheckboxProps, \"isSelected\" | \"defaultSelected\" | \"onChange\">,\n) {\n  const { isAccepted, setAccepted, registerAcceptance } = useFeaturePromo(\"FeaturePromoAcceptance\");\n\n  React.useLayoutEffect(registerAcceptance, [registerAcceptance]);\n\n  return (\n    <CheckboxCard\n      data-slot=\"feature-promo-acceptance\"\n      isSelected={isAccepted}\n      onChange={setAccepted}\n      {...props}\n    />\n  );\n}\n\n/**\n * The calls to action: `Button`s or `LinkButton`s. While a\n * `FeaturePromoAcceptance` is unchecked they are disabled, unless a button\n * sets its own `isDisabled`.\n */\nfunction FeaturePromoActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { isAccepted, hasAcceptance } = useFeaturePromo(\"FeaturePromoActions\");\n  const isDisabled = hasAcceptance && !isAccepted;\n\n  return (\n    <ButtonContext.Provider value={{ isDisabled }}>\n      <LinkContext.Provider value={{ isDisabled }}>\n        <div\n          data-slot=\"feature-promo-actions\"\n          className={cn(\n            \"flex flex-wrap gap-2 pt-2 group-data-[layout=centered]/feature-promo:justify-center\",\n            \"[&_a[data-disabled]]:pointer-events-none [&_a[data-disabled]]:opacity-50\",\n            className,\n          )}\n          {...props}\n        />\n      </LinkContext.Provider>\n    </ButtonContext.Provider>\n  );\n}\n\n/**\n * An illustration or product screenshot. Beside the content in `split`\n * (hidden when the card is narrow), below it in `centered`. Decorative by\n * default: give an `img` its own `alt` if it carries information.\n */\nfunction FeaturePromoMedia({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"feature-promo-media\"\n      className={cn(\n        \"overflow-hidden rounded-lg bg-muted ring-1 ring-foreground/10 [&_img]:size-full [&_img]:object-cover [&_img]:object-top-left\",\n        \"group-data-[layout=split]/feature-promo:@max-3xl:hidden\",\n        \"group-data-[layout=centered]/feature-promo:mx-auto group-data-[layout=centered]/feature-promo:w-full group-data-[layout=centered]/feature-promo:max-w-3xl\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * Cards below the content, one to three columns by the card's width:\n * `RadioCard`s or `CheckboxCard`s for a choice, `Item`s with `href` for\n * shortcuts, or plain cards that explain options.\n */\nfunction FeaturePromoGrid({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"feature-promo-grid\"\n      className={cn(\n        \"col-span-full grid gap-3 text-left @xl:grid-cols-2 @3xl:grid-cols-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  FeaturePromo,\n  FeaturePromoAcceptance,\n  FeaturePromoActions,\n  FeaturePromoBenefit,\n  FeaturePromoBenefits,\n  FeaturePromoContent,\n  FeaturePromoDescription,\n  FeaturePromoGrid,\n  FeaturePromoMedia,\n  FeaturePromoTitle,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/feature-promo/feature-promo.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "promo",
      "upsell",
      "locked",
      "onboarding",
      "empty-state"
    ],
    "contentHash": "d3122ff1aacf"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}