{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-table",
  "type": "registry:component",
  "title": "Pricing Table",
  "description": "Plan comparison with feature checklist, tier highlighting, CTA, and an optional monthly/annual toggle.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/pricing-table/pricing-table.tsx",
      "content": "\"use client\";\n\nimport {\n  type ButtonHTMLAttributes,\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n  useCallback,\n  useState,\n} from \"react\";\n\nimport { Check, X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button, type ButtonProps } from \"@vllnt/ui\";\n\n/**\n * One row in a {@link PricingPlan}'s feature checklist.\n *\n * @public\n */\nexport type PricingFeature = {\n  /**\n   * When `true`, the row renders a check; when `false`, an X. Pass a string\n   * (e.g. `\"5 users\"`) to render the value as the limit indicator instead.\n   */\n  included: boolean | string;\n  /** Human-readable feature description. */\n  label: ReactNode;\n};\n\n/**\n * Call-to-action descriptor for a {@link PricingPlan}.\n *\n * @public\n */\nexport type PricingPlanCta = {\n  /** Button label. */\n  label: ReactNode;\n  /** Click handler. */\n  onClick?: ButtonHTMLAttributes<HTMLButtonElement>[\"onClick\"];\n  /** Underlying {@link Button} variant. Defaults to `\"default\"`. */\n  variant?: ButtonProps[\"variant\"];\n};\n\n/**\n * Props for {@link PricingPlan}.\n *\n * @public\n */\nexport type PricingPlanProps = {\n  /** Optional badge text shown on highlighted plans (e.g. \"Most Popular\"). */\n  badge?: ReactNode;\n  /** Bottom-row call-to-action descriptor. */\n  cta?: PricingPlanCta;\n  /** Sub-headline shown under the plan name. */\n  description?: ReactNode;\n  /** Feature checklist. */\n  features?: PricingFeature[];\n  /** When `true`, the plan renders with emphasis styling. */\n  highlighted?: boolean;\n  /** Plan name (e.g. \"Free\", \"Pro\"). */\n  name: ReactNode;\n  /** Suffix shown next to the price (e.g. \"/month\"). */\n  period?: ReactNode;\n  /** Headline price. */\n  price: ReactNode;\n} & Omit<ComponentPropsWithoutRef<\"div\">, \"children\">;\n\nfunction FeatureIndicator({\n  included,\n}: {\n  included: boolean | string;\n}): ReactNode {\n  if (typeof included === \"string\") {\n    return (\n      <span\n        aria-hidden=\"true\"\n        className=\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-primary/15 text-[10px] font-semibold text-primary\"\n      >\n        ✓\n      </span>\n    );\n  }\n  if (included) {\n    return (\n      <Check\n        aria-hidden=\"true\"\n        className=\"mt-0.5 size-4 shrink-0 text-primary\"\n      />\n    );\n  }\n  return (\n    <X\n      aria-hidden=\"true\"\n      className=\"mt-0.5 size-4 shrink-0 text-muted-foreground/60\"\n    />\n  );\n}\n\nfunction FeatureRow({ feature }: { feature: PricingFeature }): ReactNode {\n  const { included, label } = feature;\n  const isLimit = typeof included === \"string\";\n  return (\n    <li className=\"flex items-start gap-2 text-sm\">\n      <FeatureIndicator included={included} />\n      <span\n        className={cn(\n          \"flex-1\",\n          included === false && \"text-muted-foreground line-through\",\n        )}\n      >\n        {label}\n        {isLimit ? (\n          <span className=\"ml-1 text-muted-foreground\">({included})</span>\n        ) : null}\n      </span>\n    </li>\n  );\n}\n\nfunction PlanBadgePill({\n  badge,\n  highlighted,\n}: {\n  badge: ReactNode;\n  highlighted: boolean;\n}): ReactNode {\n  return (\n    <span\n      className={cn(\n        \"absolute -top-3 left-1/2 -translate-x-1/2 rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide\",\n        highlighted\n          ? \"bg-primary text-primary-foreground\"\n          : \"bg-muted text-muted-foreground\",\n      )}\n    >\n      {badge}\n    </span>\n  );\n}\n\nfunction PlanHeader({\n  description,\n  name,\n}: {\n  description: ReactNode;\n  name: ReactNode;\n}): ReactNode {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <h3 className=\"text-lg font-semibold tracking-tight text-foreground\">\n        {name}\n      </h3>\n      {description ? (\n        <p className=\"text-sm text-muted-foreground\">{description}</p>\n      ) : null}\n    </div>\n  );\n}\n\nfunction PlanPrice({\n  period,\n  price,\n}: {\n  period: ReactNode;\n  price: ReactNode;\n}): ReactNode {\n  return (\n    <div className=\"flex items-baseline gap-1\">\n      <span className=\"text-3xl font-bold tracking-tight text-foreground\">\n        {price}\n      </span>\n      {period ? (\n        <span className=\"text-sm text-muted-foreground\">{period}</span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction PlanFeatures({ features }: { features: PricingFeature[] }): ReactNode {\n  return (\n    <ul className=\"flex flex-col gap-2\">\n      {features.map((feature, index) => (\n        <FeatureRow\n          feature={feature}\n          key={`${typeof feature.label === \"string\" ? feature.label : index.toString()}-${index.toString()}`}\n        />\n      ))}\n    </ul>\n  );\n}\n\ntype PlanCtaProps = {\n  cta: PricingPlanCta;\n  highlighted: boolean;\n};\n\nfunction PlanCta({ cta, highlighted }: PlanCtaProps): ReactNode {\n  const { label, onClick: handleCtaClick, variant } = cta;\n  return (\n    <Button\n      className=\"mt-auto w-full\"\n      onClick={handleCtaClick}\n      type=\"button\"\n      variant={variant ?? (highlighted ? \"default\" : \"outline\")}\n    >\n      {label}\n    </Button>\n  );\n}\n\n/**\n * Single plan column inside a {@link PricingTable}.\n *\n * @example\n * ```tsx\n * <PricingPlan\n *   name=\"Pro\"\n *   price=\"$29\"\n *   period=\"/month\"\n *   highlighted\n *   badge=\"Most Popular\"\n *   features={[\n *     { label: \"Unlimited projects\", included: true },\n *     { label: \"Storage\", included: \"100 GB\" },\n *   ]}\n *   cta={{ label: \"Start trial\", onClick: startTrial }}\n * />\n * ```\n *\n * @public\n */\nexport const PricingPlan = forwardRef<HTMLDivElement, PricingPlanProps>(\n  (props, ref) => {\n    const {\n      badge,\n      className,\n      cta,\n      description,\n      features,\n      highlighted = false,\n      name,\n      period,\n      price,\n      ...rest\n    } = props;\n    return (\n      <div\n        className={cn(\n          \"relative flex flex-col gap-6 rounded-2xl border bg-background p-6 shadow-sm transition-colors\",\n          highlighted\n            ? \"border-primary shadow-md ring-1 ring-primary/20\"\n            : \"border-border\",\n          className,\n        )}\n        ref={ref}\n        {...rest}\n      >\n        {badge ? (\n          <PlanBadgePill badge={badge} highlighted={highlighted} />\n        ) : null}\n        <PlanHeader description={description} name={name} />\n        <PlanPrice period={period} price={price} />\n        {features && features.length > 0 ? (\n          <PlanFeatures features={features} />\n        ) : null}\n        {cta ? <PlanCta cta={cta} highlighted={highlighted} /> : null}\n      </div>\n    );\n  },\n);\nPricingPlan.displayName = \"PricingPlan\";\n\n/**\n * Billing period for {@link PricingTable}'s built-in toggle.\n *\n * @public\n */\nexport type PricingPeriod = \"annual\" | \"monthly\";\n\ntype PeriodLabels = {\n  annual?: ReactNode;\n  monthly?: ReactNode;\n  /** Optional caption shown next to the annual option (e.g. \"Save 20%\"). */\n  savings?: ReactNode;\n};\n\n/**\n * Props for {@link PricingTable}.\n *\n * @public\n */\nexport type PricingTableProps = {\n  /** Period selected when uncontrolled. Defaults to `\"monthly\"`. */\n  defaultPeriod?: PricingPeriod;\n  /** Fires when the user changes the period (controlled or uncontrolled). */\n  onPeriodChange?: (period: PricingPeriod) => void;\n  /** Controlled value for the period toggle. */\n  period?: PricingPeriod;\n  /** Captions for the toggle. Defaults to `Monthly` / `Annual`. */\n  periodLabels?: PeriodLabels;\n  /** Set to `true` to render the built-in monthly/annual toggle. */\n  showPeriodToggle?: boolean;\n} & ComponentPropsWithoutRef<\"div\">;\n\ntype PeriodToggleProps = {\n  labels: PeriodLabels;\n  onChange: (period: PricingPeriod) => void;\n  period: PricingPeriod;\n};\n\nfunction PeriodToggle({\n  labels,\n  onChange,\n  period,\n}: PeriodToggleProps): ReactNode {\n  const handleSelectMonthly = useCallback(() => {\n    onChange(\"monthly\");\n  }, [onChange]);\n  const handleSelectAnnual = useCallback(() => {\n    onChange(\"annual\");\n  }, [onChange]);\n\n  return (\n    <div\n      aria-label=\"Billing period\"\n      className=\"mx-auto inline-flex items-center gap-2 rounded-full border bg-muted/40 p-1 text-sm\"\n      role=\"radiogroup\"\n    >\n      <button\n        aria-checked={period === \"monthly\"}\n        className={cn(\n          \"rounded-full px-3 py-1 transition-colors\",\n          period === \"monthly\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n        onClick={handleSelectMonthly}\n        role=\"radio\"\n        type=\"button\"\n      >\n        {labels.monthly ?? \"Monthly\"}\n      </button>\n      <button\n        aria-checked={period === \"annual\"}\n        className={cn(\n          \"inline-flex items-center gap-2 rounded-full px-3 py-1 transition-colors\",\n          period === \"annual\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n        onClick={handleSelectAnnual}\n        role=\"radio\"\n        type=\"button\"\n      >\n        {labels.annual ?? \"Annual\"}\n        {labels.savings ? (\n          <span className=\"rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary\">\n            {labels.savings}\n          </span>\n        ) : null}\n      </button>\n    </div>\n  );\n}\n\n/**\n * Plan comparison container. Lays out child {@link PricingPlan} columns\n * side-by-side on desktop and stacks them on mobile. Optionally renders a\n * monthly/annual period toggle whose value flows through `onPeriodChange`.\n *\n * @example\n * ```tsx\n * const [period, setPeriod] = useState<PricingPeriod>(\"monthly\")\n *\n * <PricingTable showPeriodToggle period={period} onPeriodChange={setPeriod}>\n *   <PricingPlan name=\"Free\" price=\"$0\" period=\"/mo\" cta={{ label: \"Start\" }} />\n *   <PricingPlan name=\"Pro\" price={period === \"monthly\" ? \"$29\" : \"$24\"} highlighted />\n * </PricingTable>\n * ```\n *\n * @public\n */\nexport const PricingTable = forwardRef<HTMLDivElement, PricingTableProps>(\n  (props, ref) => {\n    const {\n      children,\n      className,\n      defaultPeriod = \"monthly\",\n      onPeriodChange,\n      period: controlledPeriod,\n      periodLabels,\n      showPeriodToggle = false,\n      ...rest\n    } = props;\n\n    const [uncontrolledPeriod, setUncontrolledPeriod] =\n      useState<PricingPeriod>(defaultPeriod);\n    const period = controlledPeriod ?? uncontrolledPeriod;\n\n    const handlePeriodChange = useCallback(\n      (next: PricingPeriod) => {\n        if (controlledPeriod === undefined) setUncontrolledPeriod(next);\n        onPeriodChange?.(next);\n      },\n      [controlledPeriod, onPeriodChange],\n    );\n\n    return (\n      <div className={cn(\"flex flex-col gap-6\", className)} ref={ref} {...rest}>\n        {showPeriodToggle ? (\n          <PeriodToggle\n            labels={periodLabels ?? {}}\n            onChange={handlePeriodChange}\n            period={period}\n          />\n        ) : null}\n        <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n          {children}\n        </div>\n      </div>\n    );\n  },\n);\nPricingTable.displayName = \"PricingTable\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
