{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-portal",
  "title": "Billing Portal",
  "description": "Plan, usage, payment method, invoice history, billing alerts, and confirmed cancellation.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "button",
    "table",
    "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/progress-summary.json",
    "https://uaiblocks.dev/r/status-banner.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/billing-portal.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  createContext,\n  type RefObject,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\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 { ProgressSummary, type ProgressSummaryProps } from \"@/components/ui/uai/progress-summary\";\nimport {\n  StatusBanner,\n  type StatusBannerProps,\n  type StatusBannerVariant,\n} from \"@/components/ui/uai/status-banner\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const BILLING_PORTAL_VARIANTS = [\"overview\", \"stacked\", \"compact\"] as const;\nexport type BillingPortalVariant = (typeof BILLING_PORTAL_VARIANTS)[number];\nexport type BillingPortalProps = ComponentProps<\"section\"> & { variant?: BillingPortalVariant };\n\ntype PortalContext = { id: string; variant: BillingPortalVariant };\nconst Context = createContext<PortalContext | null>(null);\nfunction usePortal(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within BillingPortal`);\n  return context;\n}\nconst SectionContext = createContext<string | null>(null);\n\nconst bannerVariants: Record<BillingPortalVariant, StatusBannerVariant> = {\n  overview: \"card\",\n  stacked: \"tinted\",\n  compact: \"tinted\",\n};\nconst detailVariants: Record<BillingPortalVariant, DescriptionListVariant> = {\n  overview: \"inline\",\n  stacked: \"inline\",\n  compact: \"stacked\",\n};\nconst dialogVariants: Record<BillingPortalVariant, ConfirmationDialogVariant> = {\n  overview: \"centered\",\n  stacked: \"centered\",\n  compact: \"compact\",\n};\n\nconst billingPortalVariants = cva(\"grid min-w-0 content-start text-[13px]/[18px] text-foreground\", {\n  variants: { variant: { overview: \"gap-4\", stacked: \"gap-4\", compact: \"gap-2.5\" } },\n});\n\nexport function BillingPortal({\n  variant = \"overview\",\n  children,\n  className,\n  ...props\n}: BillingPortalProps) {\n  const id = useId();\n  return (\n    <Context.Provider value={{ id, variant }}>\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"billing-portal\"\n        className={cn(billingPortalVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      >\n        {children}\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function BillingPortalHeader({ className, ...props }: ComponentProps<\"div\">) {\n  usePortal(\"BillingPortalHeader\");\n  return (\n    <div\n      data-slot=\"billing-portal-header\"\n      className={cn(\"flex min-w-0 flex-wrap items-end justify-between gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalHeading({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"billing-portal-heading\"\n      className={cn(\"grid min-w-0 flex-[1_1_240px] gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const context = usePortal(\"BillingPortalTitle\");\n  return (\n    <h2\n      data-slot=\"billing-portal-title\"\n      className={cn(\n        \"m-0 font-semibold tracking-[-0.01em]\",\n        context.variant === \"compact\" ? \"text-[15px]/5\" : \"text-lg/6\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-title`}\n    />\n  );\n}\n\nexport function BillingPortalDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"billing-portal-description\"\n      className={cn(\"m-0 text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalActions({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"billing-portal-actions\"\n      className={cn(\"flex flex-wrap items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\ntype ScrollEdges = { start: boolean; end: boolean };\n\n/** Tracks which ends of the table scroller inside `ref` hide content, so the edges can fade. */\nfunction useScrollEdges(ref: RefObject<HTMLElement | null>) {\n  const [edges, setEdges] = useState<ScrollEdges>({ start: false, end: false });\n  useIsomorphicLayoutEffect(() => {\n    const element = ref.current?.querySelector<HTMLElement>(\"[data-slot=table-container]\");\n    if (!element) return;\n    const measure = () => {\n      const offset = Math.abs(element.scrollLeft);\n      const room = element.scrollWidth - element.clientWidth;\n      const start = offset > 1;\n      const end = room - offset > 1;\n      setEdges((previous) =>\n        previous.start === start && previous.end === end ? previous : { start, end },\n      );\n    };\n    measure();\n    element.addEventListener(\"scroll\", measure, { passive: true });\n    const observer = typeof ResizeObserver === \"function\" ? new ResizeObserver(measure) : null;\n    observer?.observe(element);\n    return () => {\n      element.removeEventListener(\"scroll\", measure);\n      observer?.disconnect();\n    };\n  }, []);\n  return edges;\n}\n\nconst billingPortalButtonVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-full border-0 font-medium whitespace-nowrap transition-[background-color,filter,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] py-0 focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid enabled:active:scale-[0.97] motion-reduce:transition-none motion-reduce:enabled:active:scale-100 [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      emphasis: {\n        primary: \"bg-primary text-primary-foreground hover:bg-primary enabled:hover:brightness-108\",\n        secondary:\n          \"bg-secondary text-secondary-foreground enabled:hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))]\",\n      },\n      compact: {\n        true: \"h-[26px] px-2.5 has-[>svg]:px-2.5 text-[12px]\",\n        false: \"h-[30px] px-[13px] has-[>svg]:px-[13px] text-[12.5px]\",\n      },\n    },\n  },\n);\n\nexport function BillingPortalButton({\n  emphasis = \"secondary\",\n  type = \"button\",\n  className,\n  ...props\n}: ComponentProps<\"button\"> & { emphasis?: \"primary\" | \"secondary\" }) {\n  const context = usePortal(\"BillingPortalButton\");\n  return (\n    <Button\n      data-slot=\"billing-portal-button\"\n      variant={emphasis === \"primary\" ? \"default\" : \"secondary\"}\n      className={cn(\n        billingPortalButtonVariants({ emphasis, compact: context.variant === \"compact\" }),\n        className,\n      )}\n      {...props}\n      type={type}\n      data-emphasis={emphasis}\n    />\n  );\n}\n\n/** Account-level billing notice, such as a failed payment. Compose Status Banner parts inside it. */\nexport function BillingPortalAlert(props: Omit<StatusBannerProps, \"variant\">) {\n  const context = usePortal(\"BillingPortalAlert\");\n  return <StatusBanner {...props} variant={bannerVariants[context.variant]} />;\n}\n\nconst billingPortalGridVariants = cva(\"grid min-w-0\", {\n  variants: {\n    variant: {\n      overview: \"grid-cols-[repeat(auto-fit,minmax(min(100%,320px),1fr))] gap-3\",\n      stacked: \"grid-cols-[minmax(0,1fr)] gap-3\",\n      compact: \"grid-cols-[repeat(auto-fit,minmax(min(100%,240px),1fr))] gap-2\",\n    },\n  },\n});\n\n/** Section layout. Overview tiles sections side by side; Stacked keeps one column. */\nexport function BillingPortalGrid({ className, ...props }: ComponentProps<\"div\">) {\n  const { variant } = usePortal(\"BillingPortalGrid\");\n  return (\n    <div\n      data-slot=\"billing-portal-grid\"\n      className={cn(billingPortalGridVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\n/** A billing card. Set `span` to let a section such as invoices fill the whole row. */\nexport function BillingPortalSection({\n  span = false,\n  className,\n  ...props\n}: ComponentProps<\"section\"> & { span?: boolean }) {\n  const { variant } = usePortal(\"BillingPortalSection\");\n  const id = useId();\n  return (\n    <SectionContext.Provider value={id}>\n      <section\n        aria-labelledby={id}\n        data-slot=\"billing-portal-section\"\n        data-span={span || undefined}\n        className={cn(\n          \"grid min-w-0 content-start border bg-card\",\n          variant === \"compact\"\n            ? \"gap-2.5 rounded-xl p-3\"\n            : \"gap-3.5 rounded-[14px] px-[18px] pt-4 pb-[18px]\",\n          span && \"col-span-full\",\n          className,\n        )}\n        {...props}\n      />\n    </SectionContext.Provider>\n  );\n}\n\nexport function BillingPortalSectionHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"billing-portal-section-header\"\n      className={cn(\"flex min-w-0 flex-wrap items-center justify-between gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalSectionTitle({ className, ...props }: ComponentProps<\"h3\">) {\n  const id = useContext(SectionContext);\n  if (!id) throw new Error(\"BillingPortalSectionTitle must be used within BillingPortalSection\");\n  return (\n    <h3\n      data-slot=\"billing-portal-section-title\"\n      className={cn(\"m-0 text-[13px]/[18px] font-medium\", className)}\n      {...props}\n      id={id}\n    />\n  );\n}\n\nexport function BillingPortalSectionDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"billing-portal-section-description\"\n      className={cn(\"m-0 text-[12.5px]/[18px] text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalSectionFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"billing-portal-section-footer\"\n      className={cn(\"flex flex-wrap items-center gap-1.5 pt-1\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The plan price. Put the billing period in a nested span. */\nexport function BillingPortalPrice({ className, ...props }: ComponentProps<\"p\">) {\n  const { variant } = usePortal(\"BillingPortalPrice\");\n  return (\n    <p\n      data-slot=\"billing-portal-price\"\n      className={cn(\n        \"m-0 flex items-baseline gap-1.5 font-semibold tracking-[-0.02em] tabular-nums\",\n        variant === \"compact\" ? \"text-xl/[26px]\" : \"text-[26px]/8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A usage meter without its own card chrome. Compose Progress Summary parts inside it. */\nexport function BillingPortalUsage(props: Omit<ProgressSummaryProps, \"variant\">) {\n  usePortal(\"BillingPortalUsage\");\n  return <ProgressSummary {...props} variant=\"inline\" />;\n}\n\n/** Payment method or billing contact facts. Compose Description List parts inside it. */\nexport function BillingPortalDetails(props: Omit<DescriptionListProps, \"variant\">) {\n  const context = usePortal(\"BillingPortalDetails\");\n  return <DescriptionList {...props} variant={detailVariants[context.variant]} />;\n}\n\n/** Invoice history. Scrolls horizontally on narrow screens instead of squeezing columns. */\nexport function BillingPortalInvoices({ children, className, ...props }: ComponentProps<\"table\">) {\n  usePortal(\"BillingPortalInvoices\");\n  const ref = useRef<HTMLDivElement>(null);\n  const edges = useScrollEdges(ref);\n  return (\n    <div\n      ref={ref}\n      data-slot=\"billing-portal-invoices-scroller\"\n      data-overflow-start={edges.start ? \"\" : undefined}\n      data-overflow-end={edges.end ? \"\" : undefined}\n      className=\"-mx-2 min-w-0 *:data-[slot=table-container]:overscroll-x-contain data-overflow-start:mask-l-from-[calc(100%-24px)] data-overflow-end:mask-r-from-[calc(100%-24px)]\"\n    >\n      <Table\n        data-slot=\"billing-portal-invoices\"\n        className={cn(\n          \"w-full min-w-[440px] border-collapse text-[length:inherit] tabular-nums\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </Table>\n    </div>\n  );\n}\n\nexport function BillingPortalInvoicesHeader({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"thead\">) {\n  return (\n    <TableHeader\n      data-slot=\"billing-portal-invoices-header\"\n      className={cn(\"[&_tr]:border-b-0\", className)}\n      {...props}\n    >\n      <TableRow className=\"hover:bg-transparent\">{children}</TableRow>\n    </TableHeader>\n  );\n}\n\nexport function BillingPortalInvoicesColumn({\n  align = \"start\",\n  className,\n  ...props\n}: Omit<ComponentProps<\"th\">, \"align\"> & { align?: \"start\" | \"end\" }) {\n  return (\n    <TableHead\n      scope=\"col\"\n      data-slot=\"billing-portal-invoices-column\"\n      className={cn(\n        \"h-auto border-b px-2.5 pt-0 pb-2 text-[12px] font-medium whitespace-nowrap text-subtle-foreground\",\n        align === \"end\" ? \"text-end\" : \"text-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalInvoicesBody({ className, ...props }: ComponentProps<\"tbody\">) {\n  return (\n    <TableBody data-slot=\"billing-portal-invoices-body\" className={cn(className)} {...props} />\n  );\n}\n\nexport function BillingPortalInvoice({ className, ...props }: ComponentProps<\"tr\">) {\n  return (\n    <TableRow\n      data-slot=\"billing-portal-invoice\"\n      className={cn(\n        \"border-b-0 transition-[background-color] duration-120 ease-[ease-out] hover:bg-accent/55 motion-reduce:transition-none [&:last-child>td]:border-b-transparent [&>td]:border-b [&>td:first-child]:rounded-l-lg [&>td:last-child]:rounded-r-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function BillingPortalInvoiceCell({\n  align = \"start\",\n  className,\n  ...props\n}: Omit<ComponentProps<\"td\">, \"align\"> & { align?: \"start\" | \"end\" }) {\n  const { variant } = usePortal(\"BillingPortalInvoiceCell\");\n  return (\n    <TableCell\n      data-slot=\"billing-portal-invoice-cell\"\n      className={cn(\n        \"px-2.5 whitespace-nowrap\",\n        variant === \"compact\" ? \"py-[7px]\" : \"py-2.5\",\n        align === \"end\" ? \"text-end\" : \"text-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Cancellation confirmation. Compose Confirmation Dialog parts inside it. */\nexport function BillingPortalCancel(props: Omit<ConfirmationDialogProps, \"variant\">) {\n  const context = usePortal(\"BillingPortalCancel\");\n  return <ConfirmationDialog {...props} variant={dialogVariants[context.variant]} />;\n}\n",
      "type": "registry:component",
      "target": "@components/uai/billing-portal.tsx"
    }
  ],
  "categories": [
    "application"
  ],
  "type": "registry:block"
}