{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-table",
  "title": "Pricing Table",
  "description": "Plan comparison with feature checklist, tier highlighting, CTA, and an optional monthly/annual toggle.",
  "dependencies": [
    "@vllnt/ui@^0.3.0",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/pricing-table/pricing-table.tsx",
      "content": "\"use client\";\n\nimport {\n  type ButtonHTMLAttributes,\n  type ComponentPropsWithoutRef,\n  type KeyboardEvent,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useRef,\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        <span className=\"sr-only\">\n          {included === false ? \"Not included: \" : \"Included: \"}\n        </span>\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 = ({\n  ref,\n  ...props\n}: PricingPlanProps & { ref?: React.Ref<HTMLDivElement> }) => {\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 ? <PlanBadgePill badge={badge} highlighted={highlighted} /> : 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};\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\nconst PERIOD_ARROW_KEYS = new Set([\n  \"ArrowDown\",\n  \"ArrowLeft\",\n  \"ArrowRight\",\n  \"ArrowUp\",\n]);\n\ntype PeriodRadioProps = {\n  buttonRef: Ref<HTMLButtonElement>;\n  children: ReactNode;\n  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;\n  onSelect: () => void;\n  selected: boolean;\n};\n\nfunction PeriodRadio({\n  buttonRef,\n  children,\n  onKeyDown,\n  onSelect,\n  selected,\n}: PeriodRadioProps): ReactNode {\n  return (\n    <button\n      aria-checked={selected}\n      className={cn(\n        \"inline-flex items-center gap-2 rounded-full px-3 py-1 transition-colors\",\n        selected\n          ? \"bg-background text-foreground shadow-sm\"\n          : \"text-muted-foreground hover:text-foreground\",\n      )}\n      onClick={onSelect}\n      onKeyDown={onKeyDown}\n      ref={buttonRef}\n      role=\"radio\"\n      tabIndex={selected ? 0 : -1}\n      type=\"button\"\n    >\n      {children}\n    </button>\n  );\n}\n\nfunction PeriodToggle({\n  labels,\n  onChange,\n  period,\n}: PeriodToggleProps): ReactNode {\n  const monthlyRef = useRef<HTMLButtonElement>(null);\n  const annualRef = useRef<HTMLButtonElement>(null);\n  const handleSelectMonthly = useCallback(() => {\n    onChange(\"monthly\");\n  }, [onChange]);\n  const handleSelectAnnual = useCallback(() => {\n    onChange(\"annual\");\n  }, [onChange]);\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (!PERIOD_ARROW_KEYS.has(event.key)) return;\n      event.preventDefault();\n      const next: PricingPeriod = period === \"monthly\" ? \"annual\" : \"monthly\";\n      onChange(next);\n      (next === \"monthly\" ? monthlyRef : annualRef).current?.focus();\n    },\n    [onChange, period],\n  );\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      <PeriodRadio\n        buttonRef={monthlyRef}\n        onKeyDown={handleKeyDown}\n        onSelect={handleSelectMonthly}\n        selected={period === \"monthly\"}\n      >\n        {labels.monthly ?? \"Monthly\"}\n      </PeriodRadio>\n      <PeriodRadio\n        buttonRef={annualRef}\n        onKeyDown={handleKeyDown}\n        onSelect={handleSelectAnnual}\n        selected={period === \"annual\"}\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      </PeriodRadio>\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 = ({\n  ref,\n  ...props\n}: PricingTableProps & { ref?: React.Ref<HTMLDivElement> }) => {\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};\nPricingTable.displayName = \"PricingTable\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
