{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "price-summary",
  "title": "Price Summary",
  "description": "A compound order total with card, plain, and compact variants.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-number-tick.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/price-summary.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check, X } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { NumberTick } from \"@/lib/uai-number-tick\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const PRICE_SUMMARY_VARIANTS = [\"card\", \"plain\", \"compact\"] as const;\nexport const PRICE_SUMMARY_TONES = [\"default\", \"muted\", \"success\"] as const;\n\nexport type PriceSummaryVariant = (typeof PRICE_SUMMARY_VARIANTS)[number];\nexport type PriceSummaryTone = (typeof PRICE_SUMMARY_TONES)[number];\n\n/**\n * Set `aria-busy` on the root while totals recalculate: the total dims and announces once when\n * done. Set `aria-busy` on an item to show a skeleton in place of just that amount.\n */\nexport type PriceSummaryProps = ComponentProps<\"section\"> & {\n  variant?: PriceSummaryVariant;\n};\n\nconst priceSummaryVariants = cva(\"w-full text-card-foreground\", {\n  variants: {\n    variant: {\n      card: \"rounded-[14px] border border-border bg-card p-[18px]\",\n      plain: \"rounded-none bg-transparent p-0\",\n      compact: \"rounded-xl border border-border bg-card p-3\",\n    },\n  },\n});\n\nfunction priceSummaryChrome(variant: PriceSummaryVariant) {\n  const compact = variant === \"compact\";\n\n  return {\n    headerClass: compact ? \"mb-3\" : \"mb-4\",\n    titleClass: compact ? \"text-[13px] leading-[18px]\" : \"text-[14px] leading-5\",\n    descriptionClass: compact ? \"mt-0.5 text-[11.5px] leading-4\" : \"mt-0.5 text-[12px] leading-4\",\n    listClass: compact ? \"gap-1.5\" : \"gap-2\",\n    itemClass: compact ? \"text-[12px] leading-4\" : \"text-[13px] leading-[18px]\",\n    totalClass: \"mt-3 pt-3\",\n    totalLabelClass: compact ? \"text-[12.5px] leading-[18px]\" : \"text-[13px] leading-[18px]\",\n    totalValueClass: compact ? \"text-[16px] leading-5\" : \"text-[22px] leading-[26px]\",\n    codeClass: compact ? \"h-5\" : \"h-5.5\",\n    noteClass: compact ? \"mt-2.5 text-[11px] leading-4\" : \"mt-3 text-[11.5px] leading-4\",\n  };\n}\n\ntype PriceSummaryContextValue = {\n  titleId: string;\n  busy: boolean;\n  chrome: ReturnType<typeof priceSummaryChrome>;\n};\n\nconst isBusy = (value: unknown) => value === true || value === \"true\";\n/** Ticks a plain-text amount with NumberTick; other children render as given. */\nfunction tickable(children: ReactNode) {\n  return typeof children === \"string\" || typeof children === \"number\" ? (\n    <NumberTick value={String(children)} />\n  ) : (\n    children\n  );\n}\n\nconst PriceSummaryContext = createContext<PriceSummaryContextValue | null>(null);\n\nfunction usePriceSummary(name: string) {\n  const context = useContext(PriceSummaryContext);\n  if (!context) throw new Error(`${name} must be used within PriceSummary`);\n  return context;\n}\n\nexport function PriceSummary({\n  variant = \"card\",\n  children,\n  className,\n  \"aria-labelledby\": ariaLabelledby,\n  \"aria-busy\": ariaBusy,\n  ...props\n}: PriceSummaryProps) {\n  const titleId = useId();\n  const chrome = priceSummaryChrome(variant);\n  const busy = isBusy(ariaBusy);\n\n  return (\n    <PriceSummaryContext.Provider value={{ titleId, busy, chrome }}>\n      <section\n        data-slot=\"price-summary\"\n        data-variant={variant}\n        aria-busy={ariaBusy}\n        className={cn(\n          priceSummaryVariants({ variant }),\n          // An error note keeps the previous total visible but dimmed.\n          \"has-[[data-slot=price-summary-note][role=alert]]:[&_[data-slot=price-summary-total]_dd]:opacity-45\",\n          className,\n        )}\n        {...props}\n        aria-labelledby={ariaLabelledby ?? titleId}\n      >\n        {children}\n      </section>\n    </PriceSummaryContext.Provider>\n  );\n}\n\nexport type PriceSummaryHeaderProps = ComponentProps<\"header\">;\n\nexport function PriceSummaryHeader({ children, className, ...props }: PriceSummaryHeaderProps) {\n  const context = usePriceSummary(\"PriceSummaryHeader\");\n\n  return (\n    <header\n      data-slot=\"price-summary-header\"\n      className={cn(context.chrome.headerClass, className)}\n      {...props}\n    >\n      {children}\n    </header>\n  );\n}\n\nexport type PriceSummaryTitleProps = ComponentProps<\"h2\">;\n\nexport function PriceSummaryTitle({ children, className, ...props }: PriceSummaryTitleProps) {\n  const context = usePriceSummary(\"PriceSummaryTitle\");\n\n  return (\n    <h2\n      data-slot=\"price-summary-title\"\n      className={cn(\"font-medium tracking-[-0.01em]\", context.chrome.titleClass, className)}\n      {...props}\n      id={context.titleId}\n    >\n      {children}\n    </h2>\n  );\n}\n\nexport type PriceSummaryDescriptionProps = ComponentProps<\"p\">;\n\nexport function PriceSummaryDescription({\n  children,\n  className,\n  ...props\n}: PriceSummaryDescriptionProps) {\n  const context = usePriceSummary(\"PriceSummaryDescription\");\n\n  return (\n    <p\n      data-slot=\"price-summary-description\"\n      className={cn(\"text-subtle-foreground\", context.chrome.descriptionClass, className)}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n\nexport type PriceSummaryListProps = ComponentProps<\"dl\">;\n\nexport function PriceSummaryList({ children, className, ...props }: PriceSummaryListProps) {\n  const context = usePriceSummary(\"PriceSummaryList\");\n\n  return (\n    <dl\n      data-slot=\"price-summary-list\"\n      className={cn(\"grid\", context.chrome.listClass, className)}\n      {...props}\n    >\n      {children}\n    </dl>\n  );\n}\n\nexport type PriceSummaryItemProps = ComponentProps<\"div\"> & {\n  label: ReactNode;\n  tone?: PriceSummaryTone;\n};\n\nconst priceSummaryToneClass: Record<PriceSummaryTone, string> = {\n  default: \"text-card-foreground\",\n  muted: \"text-muted-foreground\",\n  success: \"text-success-ink\",\n};\n\nexport function PriceSummaryItem({\n  label,\n  tone = \"default\",\n  children,\n  className,\n  ...props\n}: PriceSummaryItemProps) {\n  const context = usePriceSummary(\"PriceSummaryItem\");\n  const pending = isBusy(props[\"aria-busy\"]);\n  // While recalculating, the skeleton keeps the previous amount's width.\n  const previous = useRef(typeof children === \"string\" ? children.length : 6);\n  if (!pending && typeof children === \"string\") previous.current = children.length;\n\n  return (\n    <div\n      data-slot=\"price-summary-item\"\n      data-tone={tone}\n      className={cn(\"flex min-w-0 items-center\", context.chrome.itemClass, className)}\n      {...props}\n    >\n      <dt className=\"min-w-0 text-muted-foreground wrap-anywhere\">{label}</dt>\n      <span className=\"min-w-4 flex-1\" aria-hidden=\"true\" />\n      <dd\n        className={cn(\n          \"max-w-[58%] shrink-0 break-words text-right font-medium tabular-nums\",\n          priceSummaryToneClass[tone],\n        )}\n      >\n        {pending ? (\n          <>\n            <span className=\"sr-only\">Updating</span>\n            <span\n              aria-hidden=\"true\"\n              className=\"ml-auto block h-3 rounded-[4px] bg-[linear-gradient(90deg,var(--muted)_0%,color-mix(in_oklab,var(--muted)_55%,var(--card))_50%,var(--muted)_100%)] bg-size-[200%_100%] motion-safe:animate-skeleton-shimmer\"\n              style={{ width: `${Math.max(previous.current - 1, 3)}ch` }}\n            />\n          </>\n        ) : (\n          tickable(children)\n        )}\n      </dd>\n    </div>\n  );\n}\n\nexport type PriceSummaryCodeProps = ComponentProps<\"span\"> & {\n  /** Called after the chip's exit when the ✕ is pressed. Omit it to hide the ✕. */\n  onRemove?: () => void;\n  /** Accessible name of the ✕. Defaults to \"Remove code <children>\". */\n  removeLabel?: string;\n};\n\n/** A discount code chip, for an item's `label`. Pops in, and leaves in 120ms when removed. */\nexport function PriceSummaryCode({\n  onRemove,\n  removeLabel,\n  children,\n  className,\n  ...props\n}: PriceSummaryCodeProps) {\n  const context = usePriceSummary(\"PriceSummaryCode\");\n  const [leaving, setLeaving] = useState(false);\n  useEffect(() => {\n    if (!leaving) return;\n    const reduce = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n    const timer = window.setTimeout(() => onRemove?.(), reduce ? 0 : 120);\n    return () => window.clearTimeout(timer);\n  }, [leaving, onRemove]);\n  return (\n    <span\n      data-slot=\"price-summary-code\"\n      data-state={leaving ? \"closed\" : \"open\"}\n      className={cn(\n        \"inline-flex items-center gap-1.5 rounded-md bg-muted pr-1 pl-2 font-mono text-[11.5px] leading-4 font-medium text-foreground\",\n        context.chrome.codeClass,\n        !onRemove && \"pr-2\",\n        \"animate-in fade-in-0 zoom-in-96 duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:animate-none\",\n        \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-96 data-[state=closed]:fill-mode-forwards data-[state=closed]:duration-120 data-[state=closed]:ease-out motion-reduce:data-[state=closed]:animate-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {onRemove ? (\n        <button\n          type=\"button\"\n          aria-label={\n            removeLabel ?? `Remove code ${typeof children === \"string\" ? children : \"\"}`.trim()\n          }\n          disabled={leaving}\n          onClick={() => setLeaving(true)}\n          className=\"grid size-4 cursor-pointer place-items-center rounded-[4px] border-0 bg-transparent p-0 text-subtle-foreground transition-[color,background-color,scale] duration-[100ms,100ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] hover:bg-foreground/8 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid enabled:active:scale-[0.92] motion-reduce:transition-none motion-reduce:enabled:active:scale-100\"\n        >\n          <X className=\"size-3\" strokeWidth={2.2} aria-hidden=\"true\" />\n        </button>\n      ) : null}\n    </span>\n  );\n}\n\nexport type PriceSummarySavingsProps = ComponentProps<\"div\">;\n\n/**\n * States what the discounts save. Expands in 300ms when it gets text and collapses when its\n * children become empty, keeping the last text during the collapse.\n */\nexport function PriceSummarySavings({ children, className, ...props }: PriceSummarySavingsProps) {\n  usePriceSummary(\"PriceSummarySavings\");\n  const open = children !== null && children !== undefined && children !== false && children !== \"\";\n  const last = useRef<ReactNode>(children);\n  if (open) last.current = children;\n  return (\n    <div\n      data-slot=\"price-summary-savings\"\n      data-state={open ? \"open\" : \"closed\"}\n      aria-hidden={!open || undefined}\n      className={cn(\n        \"grid transition-[grid-template-rows,opacity] duration-300 ease-out-quint motion-reduce:transition-none\",\n        open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"min-h-0 overflow-hidden\">\n        <p\n          className={cn(\n            \"m-0 mt-3 flex h-7.5 items-center gap-2 rounded-lg bg-success/10 px-2.5 text-[12px] leading-4 font-medium text-success-ink tabular-nums\",\n          )}\n        >\n          <Check className=\"size-3.5 shrink-0\" strokeWidth={2.2} aria-hidden=\"true\" />\n          {last.current}\n        </p>\n      </div>\n    </div>\n  );\n}\n\nexport type PriceSummaryTotalProps = ComponentProps<\"div\"> & {\n  label?: ReactNode;\n  hint?: ReactNode;\n};\n\nexport function PriceSummaryTotal({\n  label = \"Total\",\n  hint,\n  children,\n  className,\n  ...props\n}: PriceSummaryTotalProps) {\n  const context = usePriceSummary(\"PriceSummaryTotal\");\n\n  return (\n    <div\n      data-slot=\"price-summary-total\"\n      className={cn(\n        \"flex items-end justify-between gap-4 border-t border-border\",\n        context.chrome.totalClass,\n        className,\n      )}\n      {...props}\n    >\n      <dt className=\"min-w-0 wrap-anywhere\">\n        <span className={cn(\"block font-medium\", context.chrome.totalLabelClass)}>{label}</span>\n        {hint ? (\n          <span className=\"mt-0.5 block text-[11.5px] leading-4 text-subtle-foreground\">\n            {hint}\n          </span>\n        ) : null}\n      </dt>\n      <dd\n        aria-live=\"polite\"\n        aria-busy={context.busy || undefined}\n        className={cn(\n          \"ml-auto min-w-0 max-w-[58%] text-right font-semibold tracking-[-0.025em] tabular-nums wrap-anywhere\",\n          \"transition-opacity duration-120 ease-out motion-reduce:transition-none\",\n          context.busy && \"opacity-45\",\n          context.chrome.totalValueClass,\n        )}\n      >\n        {tickable(children)}\n      </dd>\n    </div>\n  );\n}\n\nexport type PriceSummaryNoteProps = ComponentProps<\"p\">;\n\nexport function PriceSummaryNote({ children, className, ...props }: PriceSummaryNoteProps) {\n  const context = usePriceSummary(\"PriceSummaryNote\");\n\n  return (\n    <p\n      data-slot=\"price-summary-note\"\n      className={cn(\n        \"text-subtle-foreground\",\n        props.role === \"alert\" && \"text-destructive-ink\",\n        context.chrome.noteClass,\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/price-summary.tsx"
    }
  ],
  "categories": [
    "commerce",
    "data-display"
  ],
  "type": "registry:ui"
}