{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "subscription-management",
  "title": "Subscription Management",
  "description": "Change plans, usage limits, payment, renewal, and cancellation.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "button",
    "label",
    "radio-group",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/confirmation-dialog.json",
    "https://uaiblocks.dev/r/description-list.json",
    "https://uaiblocks.dev/r/pricing-toggle.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/subscription-management.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  type CSSProperties,\n  createContext,\n  type ReactNode,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  ConfirmationDialog,\n  type ConfirmationDialogProps,\n  type ConfirmationDialogVariant,\n} from \"@/components/ui/uai/confirmation-dialog\";\nimport {\n  DescriptionList,\n  type DescriptionListProps,\n  type DescriptionListVariant,\n} from \"@/components/ui/uai/description-list\";\nimport { PricingToggle, type PricingToggleVariant } from \"@/components/ui/uai/pricing-toggle\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const SUBSCRIPTION_MANAGEMENT_VARIANTS = [\"split\", \"stacked\", \"compact\"] as const;\nexport type SubscriptionManagementVariant = (typeof SUBSCRIPTION_MANAGEMENT_VARIANTS)[number];\nexport type SubscriptionManagementStatusTone = \"active\" | \"trial\" | \"past-due\" | \"canceled\";\nexport type SubscriptionManagementProps = ComponentProps<\"section\"> & {\n  variant?: SubscriptionManagementVariant;\n};\n\ntype ManagementContext = { id: string; variant: SubscriptionManagementVariant };\nconst Context = createContext<ManagementContext | null>(null);\nfunction useManagement(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within SubscriptionManagement`);\n  return context;\n}\n\nconst toggleVariants: Record<SubscriptionManagementVariant, PricingToggleVariant> = {\n  split: \"segmented\",\n  stacked: \"pill\",\n  compact: \"compact\",\n};\nconst listVariants: Record<SubscriptionManagementVariant, DescriptionListVariant> = {\n  split: \"inline\",\n  stacked: \"inline\",\n  compact: \"stacked\",\n};\nconst dialogVariants: Record<SubscriptionManagementVariant, ConfirmationDialogVariant> = {\n  split: \"centered\",\n  stacked: \"sheet\",\n  compact: \"compact\",\n};\nconst statusVariants = cva(\n  \"inline-flex h-[22px] items-center gap-1.5 rounded-full px-2 text-[11.5px] font-medium\",\n  {\n    variants: {\n      tone: {\n        active: \"bg-success/14 text-success-ink\",\n        trial: \"bg-primary/14 text-primary-ink\",\n        \"past-due\": \"bg-warning/14 text-warning-ink\",\n        canceled:\n          \"bg-muted-foreground/14 text-[color-mix(in_oklab,var(--muted-foreground)_80%,var(--foreground))]\",\n      },\n    },\n  },\n);\nconst statusDotColor: Record<SubscriptionManagementStatusTone, string> = {\n  active: \"bg-success\",\n  trial: \"bg-primary\",\n  \"past-due\": \"bg-warning\",\n  canceled: \"bg-muted-foreground\",\n};\n\nconst subscriptionManagementVariants = cva(\n  \"@container box-border min-w-0 text-[13px]/[18px] text-foreground\",\n  {\n    variants: {\n      variant: {\n        split: \"\",\n        stacked: \"mx-auto max-w-[640px]\",\n        compact: \"mx-auto max-w-[480px]\",\n      },\n    },\n  },\n);\n\n/** Plan changes, usage limits, payment, renewal, and cancellation for one subscription. */\nexport function SubscriptionManagement({\n  variant = \"split\",\n  children,\n  className,\n  ...props\n}: SubscriptionManagementProps) {\n  const id = useId();\n  return (\n    <Context.Provider value={{ id, variant }}>\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"subscription-management\"\n        className={cn(subscriptionManagementVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      >\n        <div\n          data-slot=\"subscription-management-layout\"\n          className={cn(\n            \"grid min-w-0 items-start\",\n            variant === \"compact\" ? \"gap-2.5\" : \"gap-4\",\n            variant === \"split\" &&\n              \"@min-[760px]:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)] @min-[760px]:gap-x-6\",\n          )}\n        >\n          {children}\n        </div>\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function SubscriptionManagementHeader({ className, ...props }: ComponentProps<\"header\">) {\n  const { variant } = useManagement(\"SubscriptionManagementHeader\");\n  return (\n    <header\n      data-slot=\"subscription-management-header\"\n      className={cn(\n        \"grid min-w-0 gap-1\",\n        variant === \"split\" && \"@min-[760px]:col-span-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function SubscriptionManagementTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const { id, variant } = useManagement(\"SubscriptionManagementTitle\");\n  return (\n    <h2\n      data-slot=\"subscription-management-title\"\n      className={cn(\n        \"m-0 font-semibold tracking-[-0.015em]\",\n        variant === \"compact\" ? \"text-lg/[1.2]\" : \"text-[22px]/[1.2]\",\n        className,\n      )}\n      {...props}\n      id={`${id}-title`}\n    />\n  );\n}\n\nexport function SubscriptionManagementDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"subscription-management-description\"\n      className={cn(\"m-0 text-pretty text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** A column in the split layout; stacks on narrow containers. */\nexport function SubscriptionManagementColumn({ className, ...props }: ComponentProps<\"div\">) {\n  const { variant } = useManagement(\"SubscriptionManagementColumn\");\n  return (\n    <div\n      data-slot=\"subscription-management-column\"\n      className={cn(\n        \"grid min-w-0 content-start\",\n        variant === \"compact\" ? \"gap-2.5\" : \"gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type SubscriptionManagementPanelProps = ComponentProps<\"section\"> & {\n  title: ReactNode;\n  /** Optional element beside the title, such as a status badge. */\n  aside?: ReactNode;\n};\n\n/** A titled card for the current plan, usage, payment, or cancellation. */\nexport function SubscriptionManagementPanel({\n  title,\n  aside,\n  children,\n  className,\n  ...props\n}: SubscriptionManagementPanelProps) {\n  const { variant } = useManagement(\"SubscriptionManagementPanel\");\n  const id = useId();\n  return (\n    <section\n      aria-labelledby={id}\n      data-slot=\"subscription-management-panel\"\n      className={cn(\n        \"box-border grid min-w-0 bg-card\",\n        variant === \"compact\" ? \"gap-2.5 rounded-xl p-3\" : \"gap-3.5 rounded-[14px] p-4\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-wrap items-center justify-between gap-2\">\n        <h3 id={id} className=\"m-0 text-[13px]/5 font-medium\">\n          {title}\n        </h3>\n        {aside}\n      </div>\n      {children}\n    </section>\n  );\n}\n\nexport type SubscriptionManagementStatusProps = ComponentProps<\"span\"> & {\n  tone?: SubscriptionManagementStatusTone;\n};\n\n/** Subscription status as a tinted text pill with a colored dot. */\nexport function SubscriptionManagementStatus({\n  tone = \"active\",\n  children,\n  className,\n  ...props\n}: SubscriptionManagementStatusProps) {\n  return (\n    <span\n      data-slot=\"subscription-management-status\"\n      className={cn(statusVariants({ tone }), className)}\n      {...props}\n      data-tone={tone}\n    >\n      <span aria-hidden=\"true\" className={cn(\"size-1.5 rounded-full\", statusDotColor[tone])} />\n      {children}\n    </span>\n  );\n}\n\n/** The current plan price, large and tabular. */\nexport function SubscriptionManagementPrice({ className, ...props }: ComponentProps<\"p\">) {\n  const { variant } = useManagement(\"SubscriptionManagementPrice\");\n  return (\n    <p\n      data-slot=\"subscription-management-price\"\n      className={cn(\n        \"m-0 mt-0.5 flex flex-wrap items-baseline gap-1.5 font-semibold tracking-[-0.015em] tabular-nums\",\n        variant === \"compact\" ? \"text-xl/[1.1]\" : \"text-[26px]/[1.1]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function SubscriptionManagementPeriod({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"subscription-management-period\"\n      className={cn(\"text-[12.5px] font-normal tracking-normal text-subtle-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** Renewal or end date text. */\nexport function SubscriptionManagementRenewal({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"subscription-management-renewal\"\n      className={cn(\"m-0 text-[12.5px]/[18px] text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function SubscriptionManagementMeters({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"subscription-management-meters\"\n      className={cn(\"grid gap-3.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type SubscriptionManagementMeterProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  label: ReactNode;\n  value: number;\n  max: number;\n  /** Visible and announced usage text, for example \"8 of 10 seats\". */\n  valueText: string;\n};\n\n/** A usage limit. Turns warning-toned at 80% so approaching limits are visible. */\nexport function SubscriptionManagementMeter({\n  label,\n  value,\n  max,\n  valueText,\n  className,\n  ...props\n}: SubscriptionManagementMeterProps) {\n  useManagement(\"SubscriptionManagementMeter\");\n  const id = useId();\n  const ratio = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0;\n  const near = ratio >= 0.8;\n  return (\n    <div\n      data-slot=\"subscription-management-meter\"\n      className={cn(\"grid min-w-0 gap-1.5\", className)}\n      {...props}\n    >\n      <div className=\"flex justify-between gap-2\">\n        <span id={id} className=\"font-medium\">\n          {label}\n        </span>\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"text-[12.5px] tabular-nums\",\n            near ? \"text-warning-ink\" : \"text-subtle-foreground\",\n          )}\n        >\n          {valueText}\n        </span>\n      </div>\n      {/* biome-ignore lint/a11y/useSemanticElements: native meter cannot be styled consistently across browsers. */}\n      <div\n        role=\"meter\"\n        aria-labelledby={id}\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-valuetext={valueText}\n        data-near-limit={near || undefined}\n        className=\"h-1.5 overflow-hidden rounded-full bg-muted\"\n      >\n        <div\n          className={cn(\n            \"h-full w-full rounded-full transition-[translate] duration-300 ease-out-quint motion-reduce:transition-none\",\n            near ? \"bg-warning\" : \"bg-primary\",\n          )}\n          // A full-width fill slides in from the left; the track clips the hidden part.\n          style={{ translate: `${ratio * 100 - 100}% 0` }}\n        />\n      </div>\n    </div>\n  );\n}\n\ntype PlansContext = {\n  name: string;\n  current: string;\n  selected: string;\n  select: (plan: string) => void;\n};\nconst PlansContext = createContext<PlansContext | null>(null);\nfunction usePlans(part: string) {\n  const context = useContext(PlansContext);\n  if (!context) throw new Error(`${part} must be used within SubscriptionManagementPlans`);\n  return context;\n}\n\nexport type SubscriptionManagementPlansProps = Omit<ComponentProps<\"form\">, \"onSubmit\"> & {\n  /** The plan the account is on now. */\n  currentPlan: string;\n  /** Billing period shared with PricingToggleList and PricingTogglePrice children. */\n  period?: string;\n  defaultPeriod?: string;\n  onPeriodChange?: (period: string) => void;\n  /** Called with the chosen plan and billing period when the change is submitted. */\n  onPlanChange?: (plan: string, period: string) => void;\n};\n\n/** The change-plan form. Compose PricingToggle parts, plan options, and the submit button. */\nexport function SubscriptionManagementPlans({\n  currentPlan,\n  period,\n  defaultPeriod = \"\",\n  onPeriodChange,\n  onPlanChange,\n  children,\n  className,\n  ...props\n}: SubscriptionManagementPlansProps) {\n  const { variant } = useManagement(\"SubscriptionManagementPlans\");\n  const name = useId();\n  const [selected, setSelected] = useState(currentPlan);\n  const [internalPeriod, setInternalPeriod] = useState(defaultPeriod);\n  const currentPeriod = period ?? internalPeriod;\n  return (\n    <PlansContext.Provider value={{ name, current: currentPlan, selected, select: setSelected }}>\n      <form\n        data-slot=\"subscription-management-plans\"\n        className={cn(\"min-w-0\", className)}\n        {...props}\n        onSubmit={(event) => {\n          event.preventDefault();\n          onPlanChange?.(selected, currentPeriod);\n        }}\n      >\n        <PricingToggle\n          variant={toggleVariants[variant]}\n          value={currentPeriod}\n          onValueChange={(next) => {\n            if (period === undefined) setInternalPeriod(next);\n            onPeriodChange?.(next);\n          }}\n          className={variant === \"compact\" ? \"gap-2.5\" : \"gap-3.5\"}\n        >\n          {children}\n        </PricingToggle>\n      </form>\n    </PlansContext.Provider>\n  );\n}\n\nexport type SubscriptionManagementPlanOptionsProps = Omit<\n  ComponentProps<typeof RadioGroup>,\n  \"value\" | \"defaultValue\" | \"onValueChange\" | \"name\"\n>;\n\n/** A radio group of plans. */\nexport function SubscriptionManagementPlanOptions({\n  \"aria-label\": ariaLabel = \"Plans\",\n  className,\n  ...props\n}: SubscriptionManagementPlanOptionsProps) {\n  const plans = usePlans(\"SubscriptionManagementPlanOptions\");\n  return (\n    <RadioGroup\n      aria-label={ariaLabel}\n      data-slot=\"subscription-management-plan-options\"\n      className={cn(\n        \"grid grid-cols-[repeat(auto-fit,minmax(min(100%,160px),1fr))] gap-2\",\n        className,\n      )}\n      {...props}\n      name={plans.name}\n      value={plans.selected}\n      onValueChange={plans.select}\n    />\n  );\n}\n\nexport type SubscriptionManagementPlanOptionProps = Omit<\n  ComponentProps<typeof RadioGroupItem>,\n  \"value\" | \"children\" | \"style\"\n> & {\n  /** Applied to the option's label, which carries the visible card. */\n  style?: CSSProperties;\n  value: string;\n  children: ReactNode;\n  /** Text appended to the current plan, announced with its name. */\n  currentLabel?: ReactNode;\n};\n\n/** One plan choice. Its label holds the name, price, and limits. */\nexport function SubscriptionManagementPlanOption({\n  value,\n  children,\n  currentLabel = \"Current plan\",\n  className,\n  style,\n  ...props\n}: SubscriptionManagementPlanOptionProps) {\n  const plans = usePlans(\"SubscriptionManagementPlanOption\");\n  const { variant } = useManagement(\"SubscriptionManagementPlanOption\");\n  const current = plans.current === value;\n  return (\n    <Label\n      data-slot=\"subscription-management-plan-option\"\n      className={cn(\n        \"relative grid min-w-0 cursor-pointer content-start items-stretch gap-1 bg-muted text-[length:inherit] leading-[inherit] font-normal select-auto transition-[background-color,box-shadow] duration-120 ease-[ease-out] hover:bg-[color-mix(in_oklab,var(--muted)_88%,var(--foreground))] has-[[role=radio]:focus-visible]:outline-2 has-[[role=radio]:focus-visible]:outline-offset-2 has-[[role=radio]:focus-visible]:outline-ring has-[[role=radio][aria-checked=true]]:bg-[color-mix(in_oklab,var(--primary)_8%,var(--card))] has-[[role=radio][aria-checked=true]]:shadow-[inset_0_0_0_1.5px_var(--primary)] has-[[role=radio]:disabled]:cursor-not-allowed has-[[role=radio]:disabled]:opacity-55 motion-reduce:transition-none\",\n        variant === \"compact\" ? \"rounded-lg p-2.5\" : \"rounded-[10px] p-3\",\n        className,\n      )}\n      style={style}\n    >\n      <RadioGroupItem\n        {...props}\n        value={value}\n        className=\"absolute inset-0 m-0 aspect-auto size-auto cursor-[inherit] rounded-[inherit] border-0 opacity-0 shadow-none focus-visible:ring-0 dark:bg-transparent\"\n      />\n      {children}\n      {current ? (\n        <span className=\"mt-1 justify-self-start rounded-full bg-foreground/10 px-2 py-0.5 text-[11.5px]/4 font-medium text-muted-foreground\">\n          {currentLabel}\n        </span>\n      ) : null}\n    </Label>\n  );\n}\n\nexport function SubscriptionManagementPlanName({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"subscription-management-plan-name\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function SubscriptionManagementPlanDetail({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"subscription-management-plan-detail\"\n      className={cn(\"text-xs/4 text-subtle-foreground tabular-nums\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type SubscriptionManagementPlanSubmitProps = ComponentProps<\"button\">;\n\n/** Submits the plan change. Disabled until a different plan is selected. */\nexport function SubscriptionManagementPlanSubmit({\n  children = \"Change plan\",\n  disabled,\n  className,\n  ...props\n}: SubscriptionManagementPlanSubmitProps) {\n  const plans = usePlans(\"SubscriptionManagementPlanSubmit\");\n  const { variant } = useManagement(\"SubscriptionManagementPlanSubmit\");\n  const blocked = disabled || plans.selected === plans.current;\n  return (\n    <Button\n      data-slot=\"subscription-management-plan-submit\"\n      variant=\"default\"\n      className={cn(\n        \"justify-self-start rounded-full border-0 px-3.5 py-0 text-[13px] transition-[background-color,filter,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring has-[>svg]:px-3.5 enabled:hover:brightness-108 enabled:active:scale-[0.97] disabled:pointer-events-auto disabled:opacity-100 motion-reduce:transition-none motion-reduce:enabled:active:scale-100\",\n        variant === \"compact\" ? \"h-7\" : \"h-8\",\n        blocked\n          ? \"cursor-not-allowed bg-secondary text-subtle-foreground hover:bg-secondary\"\n          : \"cursor-pointer bg-primary text-primary-foreground hover:bg-primary\",\n        className,\n      )}\n      {...props}\n      type=\"submit\"\n      disabled={blocked}\n    >\n      {children}\n    </Button>\n  );\n}\n\n/** Payment method and billing facts. Compose DescriptionList parts inside. */\nexport function SubscriptionManagementPayment(props: Omit<DescriptionListProps, \"variant\">) {\n  const { variant } = useManagement(\"SubscriptionManagementPayment\");\n  return <DescriptionList {...props} variant={listVariants[variant]} />;\n}\n\n/** Cancellation. Compose ConfirmationDialog parts inside; the block picks the dialog style. */\nexport function SubscriptionManagementCancel(props: Omit<ConfirmationDialogProps, \"variant\">) {\n  const { variant } = useManagement(\"SubscriptionManagementCancel\");\n  return <ConfirmationDialog {...props} variant={dialogVariants[variant]} />;\n}\n",
      "type": "registry:component",
      "target": "@components/uai/subscription-management.tsx"
    }
  ],
  "categories": [
    "commerce",
    "billing"
  ],
  "type": "registry:block"
}