{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wallet",
  "title": "Wallet",
  "description": "A customer's stored balance, shown as a leather wallet. Saved cards sit in it, and picking one pulls it out of the pocket.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json",
    "https://bevel-ui.vercel.app/r/payment-card.json",
    "https://bevel-ui.vercel.app/r/amount.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/wallet.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { Meter as MeterPrimitive } from \"@base-ui/react/meter\"\nimport { Radio as RadioPrimitive } from \"@base-ui/react/radio\"\nimport { RadioGroup } from \"@base-ui/react/radio-group\"\nimport { Separator as SeparatorPrimitive } from \"@base-ui/react/separator\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { cn } from \"cn\"\nimport { Amount } from \"@/components/ui/amount\"\nimport { isDigitPart, useAmountParts, type AmountFormat } from \"@/lib/format-amount\"\nimport { PaymentCard, type PaymentCardProps, type PaymentCardTone } from \"@/components/ui/payment-card\"\n\ntype WalletVariant = \"saddle\" | \"noir\" | \"oxblood\"\ntype WalletStatus = \"default\" | \"low\" | \"empty\" | \"insufficient\"\n\ntype WalletProps = useRender.ComponentProps<\"div\"> & {\n  /** Leather finish. */\n  variant?: WalletVariant\n  /** Drives the status light, meter fill and any `group-data-[status=*]/wallet` styling. */\n  status?: WalletStatus\n}\n\nfunction cssVars(vars: Record<`--${string}`, string | number>) {\n  return vars as React.CSSProperties\n}\n\nconst WalletContext = React.createContext<{ status: WalletStatus }>({ status: \"default\" })\n\nfunction Wallet({\n  className,\n  render,\n  variant = \"saddle\",\n  status = \"default\",\n  ...props\n}: WalletProps) {\n  const context = React.useMemo(() => ({ status }), [status])\n  const element = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(\n      {\n        \"data-slot\": \"wallet\",\n        \"data-variant\": variant,\n        \"data-status\": status,\n        className: cn(\n          \"group/wallet bevel-leather bevel-object relative isolate flex w-full max-w-sm flex-col rounded-(--bevel-radius-lg) text-left text-(--wallet-foreground) outline-none\",\n          \"focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          // Stitching follows the outline: inner radius = outer radius - inset.\n          \"before:bevel-stitch before:pointer-events-none before:absolute before:inset-(--bevel-stitch-inset) before:rounded-[calc(var(--bevel-radius-lg)-var(--bevel-stitch-inset))]\",\n          \"forced-colors:border forced-colors:border-[CanvasText]\",\n          className\n        ),\n      } as React.ComponentProps<\"div\">,\n      props\n    ),\n  })\n  return <WalletContext value={context}>{element}</WalletContext>\n}\n\ntype WalletCardsContextValue = {\n  selectable: boolean\n  /** True once the stack has mounted; read by each card on its first render only. */\n  mountedRef: React.RefObject<boolean>\n}\nconst WalletCardsContext = React.createContext<WalletCardsContextValue>({\n  selectable: false,\n  mountedRef: { current: false },\n})\nconst WalletCardPositionContext = React.createContext({ index: 0, count: 1 })\n\ntype WalletCardsProps = Omit<RadioGroup.Props<string>, \"render\" | \"style\"> & {\n  style?: React.CSSProperties\n  /** Cards the slot shows spread out. More than this bunch together; the slot never grows. */\n  slots?: number\n}\n\n/**\n * The cards tucked in the wallet, back to front. Passing `value`,\n * `defaultValue` or `onValueChange` makes them a radio group: the chosen\n * card is pulled up out of the pocket.\n */\nfunction WalletCards({\n  className,\n  style,\n  children,\n  value,\n  defaultValue,\n  onValueChange,\n  slots = 3,\n  ...props\n}: WalletCardsProps) {\n  const selectable = value !== undefined || defaultValue !== undefined || onValueChange !== undefined\n  const items = React.Children.toArray(children)\n\n  // Cards present on first render are already in the wallet; only cards\n  // added afterwards slide in.\n  const mountedRef = React.useRef(false)\n  React.useEffect(() => {\n    mountedRef.current = true\n  }, [])\n  const context = React.useMemo(() => ({ selectable, mountedRef }), [selectable])\n\n  const shared = {\n    \"data-slot\": \"wallet-cards\",\n    \"data-selectable\": selectable ? \"\" : undefined,\n    // Crowded: cards behind the front one show only their edges, so their print hides.\n    \"data-crowded\": !selectable && items.length > slots ? \"\" : undefined,\n    // Height and card spacing live in wallet.css; the slot never animates its size.\n    className: cn(\"relative mx-5 mt-4 shrink-0\", !selectable && \"pointer-events-none\", className),\n    style: { ...cssVars({ \"--wallet-card-count\": items.length, \"--wallet-card-slots\": slots }), ...style },\n    children: items.map((child, index) => (\n      <WalletCardPositionContext\n        key={React.isValidElement(child) ? child.key : index}\n        value={{ index, count: items.length }}\n      >\n        {child}\n      </WalletCardPositionContext>\n    )),\n  }\n\n  return (\n    <WalletCardsContext value={context}>\n      {selectable ? (\n        <RadioGroup\n          aria-label=\"Payment methods\"\n          value={value}\n          defaultValue={defaultValue}\n          onValueChange={onValueChange}\n          {...props}\n          {...shared}\n        />\n      ) : (\n        <div {...(props as React.ComponentProps<\"div\">)} {...shared} />\n      )}\n    </WalletCardsContext>\n  )\n}\n\n// Resting tilt for each card behind the front one; the front card sits square.\nconst cardTilts = [-1.25, 0.75, -0.5, 1, -0.75]\nconst cardTones: PaymentCardTone[] = [\"sapphire\", \"gold\", \"graphite\"]\n\ntype WalletCardProps = Omit<PaymentCardProps, \"render\"> & {\n  /** Selection value when the cards are a radio group. Defaults to the card's position. */\n  value?: string\n  disabled?: boolean\n}\n\n/** A PaymentCard placed in the wallet's stack. */\nfunction WalletCard({ className, style, value, disabled, tone, children, ...props }: WalletCardProps) {\n  const { selectable, mountedRef } = React.useContext(WalletCardsContext)\n  const { index, count } = React.useContext(WalletCardPositionContext)\n  const [enters] = React.useState(() => mountedRef.current)\n  const fromFront = count - 1 - index\n\n  return (\n    <PaymentCard\n      tone={tone ?? cardTones[index % cardTones.length]}\n      render={\n        selectable ? (\n          <RadioPrimitive.Root value={value ?? String(index)} disabled={disabled} />\n        ) : undefined\n      }\n      aria-hidden={selectable || children ? undefined : true}\n      data-wallet-card=\"\"\n      data-enter={enters ? \"\" : undefined}\n      className={cn(\n        // Only the top strip shows above the pocket, so the card is shorter than a real one.\n        \"absolute inset-x-0 top-0 aspect-auto h-32\",\n        \"[translate:0_calc(var(--wallet-card-index)*var(--wallet-card-gap)+var(--wallet-card-lift))] rotate-(--wallet-card-tilt)\",\n        \"transition-[translate,rotate,opacity,filter] duration-(--bevel-duration-move) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n        \"group-hover/wallet:not-data-checked:[--wallet-card-lift:calc(var(--bevel-lift)*-1)] group-hover/wallet:not-data-checked:delay-[calc(var(--wallet-card-index)*var(--bevel-stagger))]\",\n        // Pulled out with a spring; it straightens as it comes up.\n        \"data-checked:rotate-0 data-checked:ease-(--bevel-ease-spring) data-checked:[--wallet-card-lift:calc(var(--wallet-card-pull)*-1)]\",\n        \"data-enter:starting:opacity-0 data-enter:starting:[--wallet-card-lift:calc(var(--wallet-card-enter)*-1)]\",\n        \"[&[role=radio]]:cursor-pointer [&[role=radio]]:not-data-checked:brightness-(--bevel-dim)\",\n        // In a crowded slot only the front card's print shows; the rest stay readable to screen readers.\n        \"in-data-crowded:not-last:*:opacity-0\",\n        className\n      )}\n      style={{\n        ...cssVars({\n          \"--wallet-card-index\": index,\n          \"--wallet-card-tilt\": `${fromFront === 0 ? 0 : cardTilts[(fromFront - 1) % cardTilts.length]}deg`,\n        }),\n        ...style,\n      }}\n      {...props}\n    >\n      {children}\n    </PaymentCard>\n  )\n}\n\nfunction WalletPocket({ className, children, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"wallet-pocket\"\n      className={cn(\n        \"wallet-pocket relative z-10 flex flex-col gap-4 rounded-t-(--bevel-radius-xs) rounded-b-(--bevel-radius-lg) px-5 pt-9 pb-5\",\n        \"forced-colors:border-t forced-colors:border-[CanvasText]\",\n        className\n      )}\n      {...props}\n    >\n      {/* The pocket's top is a fold, so it is stitched on three sides only. */}\n      <span\n        aria-hidden\n        className=\"bevel-stitch pointer-events-none absolute inset-x-(--bevel-stitch-inset) top-0 bottom-(--bevel-stitch-inset) rounded-b-[calc(var(--bevel-radius-lg)-var(--bevel-stitch-inset))] border-t-0\"\n      />\n      {children}\n    </div>\n  )\n}\n\nfunction WalletHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"wallet-header\"\n      className={cn(\"flex items-center justify-between gap-3\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletLabel({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"wallet-label\"\n      className={cn(\n        \"bevel-deboss text-(length:--bevel-label) font-semibold tracking-(--bevel-tracking-label) text-(--wallet-muted-foreground) uppercase\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nconst statusText: Record<WalletStatus, string> = {\n  default: \"Active\",\n  low: \"Low balance\",\n  empty: \"Empty\",\n  insufficient: \"Insufficient\",\n}\n\n/** Status light. Reads the wallet's `status`; pass children to override the text. */\nfunction WalletStatusLight({ className, children, ...props }: React.ComponentProps<\"span\">) {\n  const { status } = React.useContext(WalletContext)\n  return (\n    <span\n      data-slot=\"wallet-status\"\n      className={cn(\n        \"bevel-deboss inline-flex items-center gap-1.5 text-(length:--bevel-label) font-medium text-(--wallet-muted-foreground)\",\n        className\n      )}\n      {...props}\n    >\n      <span aria-hidden className=\"bevel-led size-2 shrink-0 rounded-full\" />\n      {children ?? statusText[status]}\n    </span>\n  )\n}\n\nconst isMinor = (type: Intl.NumberFormatPartTypes) =>\n  type === \"currency\" || type === \"decimal\" || type === \"fraction\" || type === \"literal\"\nconst isMuted = (type: Intl.NumberFormatPartTypes) =>\n  type === \"currency\" || type === \"decimal\" || type === \"fraction\"\n\nfunction WalletBalance({\n  value,\n  currency,\n  locale,\n  format,\n  className,\n  ...props\n}: AmountFormat & Omit<React.ComponentProps<\"p\">, \"children\">) {\n  const parts = useAmountParts({ value, currency, locale, format })\n  return (\n    <p\n      data-slot=\"wallet-balance\"\n      className={cn(\"bevel-deboss flex items-baseline font-semibold tracking-tight\", className)}\n      {...props}\n    >\n      {parts.map((part, i) => (\n        <span\n          key={i}\n          data-part={part.type}\n          className={cn(\n            \"text-(length:--bevel-amount) leading-none\",\n            isDigitPart(part.type) && \"tabular-nums\",\n            isMinor(part.type) && \"text-(length:--bevel-amount-minor)\",\n            isMuted(part.type) && \"text-(--wallet-muted-foreground)\",\n            part.type === \"currency\" && \"mr-0.5 self-start pt-1\",\n            part.type === \"minusSign\" && \"mr-0.5\"\n          )}\n        >\n          {part.value}\n        </span>\n      ))}\n    </p>\n  )\n}\n\n/** Inline amount for detail rows; same formatting as the balance. */\nfunction WalletAmount(props: React.ComponentProps<typeof Amount>) {\n  return <Amount data-wallet-amount=\"\" {...props} />\n}\n\nfunction WalletDetails({ className, ...props }: React.ComponentProps<\"dl\">) {\n  return (\n    <dl data-slot=\"wallet-details\" className={cn(\"flex flex-col gap-1.5 text-sm\", className)} {...props} />\n  )\n}\n\nfunction WalletDetail({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"wallet-detail\"\n      className={cn(\"bevel-deboss flex items-baseline justify-between gap-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletDetailLabel({ className, ...props }: React.ComponentProps<\"dt\">) {\n  return (\n    <dt\n      data-slot=\"wallet-detail-label\"\n      className={cn(\"text-(--wallet-muted-foreground)\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletDetailValue({ className, ...props }: React.ComponentProps<\"dd\">) {\n  return <dd data-slot=\"wallet-detail-value\" className={cn(\"font-medium\", className)} {...props} />\n}\n\nfunction WalletSeparator({ className, ...props }: SeparatorPrimitive.Props) {\n  return (\n    <SeparatorPrimitive\n      data-slot=\"wallet-separator\"\n      className={cn(\"bevel-stitch -mx-1 border-x-0 border-b-0\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletMeter({ className, children, ...props }: MeterPrimitive.Root.Props) {\n  return (\n    <MeterPrimitive.Root\n      data-slot=\"wallet-meter\"\n      className={cn(\"grid grid-cols-[1fr_auto] items-baseline gap-x-3 gap-y-2\", className)}\n      {...props}\n    >\n      {children}\n      <MeterPrimitive.Track\n        data-slot=\"wallet-meter-track\"\n        className=\"bevel-recessed col-span-full h-2 overflow-hidden rounded-full p-px\"\n      >\n        <MeterPrimitive.Indicator\n          data-slot=\"wallet-meter-indicator\"\n          className=\"bevel-fill h-full rounded-full forced-colors:bg-[Highlight]\"\n        />\n      </MeterPrimitive.Track>\n    </MeterPrimitive.Root>\n  )\n}\n\nfunction WalletMeterLabel({ className, ...props }: MeterPrimitive.Label.Props) {\n  return (\n    <MeterPrimitive.Label\n      data-slot=\"wallet-meter-label\"\n      className={cn(\"bevel-deboss text-sm text-(--wallet-muted-foreground)\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletMeterValue({ className, ...props }: MeterPrimitive.Value.Props) {\n  return (\n    <MeterPrimitive.Value\n      data-slot=\"wallet-meter-value\"\n      className={cn(\"bevel-deboss text-sm font-medium tabular-nums\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction WalletFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div data-slot=\"wallet-footer\" className={cn(\"flex items-center gap-2\", className)} {...props} />\n  )\n}\n\nfunction WalletAction({ className, ...props }: ButtonPrimitive.Props) {\n  return (\n    <ButtonPrimitive\n      data-slot=\"wallet-action\"\n      className={cn(\n        \"bevel-raised bevel-deboss inline-flex h-9 flex-1 items-center justify-center gap-1.5 rounded-(--bevel-radius-sm) px-3 text-sm font-medium text-(--wallet-foreground) outline-none select-none\",\n        \"transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n        \"active:translate-y-0.5 active:bevel-pressed data-pressed:translate-y-0.5 data-pressed:bevel-pressed\",\n        \"focus-visible:ring-3 focus-visible:ring-(--wallet-foreground)/40\",\n        \"data-disabled:cursor-not-allowed data-disabled:opacity-60 data-disabled:active:translate-y-0\",\n        \"forced-colors:border forced-colors:border-[ButtonText]\",\n        \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Wallet,\n  WalletCards,\n  WalletCard,\n  WalletPocket,\n  WalletHeader,\n  WalletLabel,\n  WalletStatusLight as WalletStatus,\n  WalletBalance,\n  WalletAmount,\n  WalletDetails,\n  WalletDetail,\n  WalletDetailLabel,\n  WalletDetailValue,\n  WalletSeparator,\n  WalletMeter,\n  WalletMeterLabel,\n  WalletMeterValue,\n  WalletFooter,\n  WalletAction,\n}\nexport type {\n  WalletProps,\n  WalletCardsProps,\n  WalletCardProps,\n  WalletVariant,\n  WalletStatus as WalletStatusValue,\n}\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--wallet-leather": "oklch(0.47 0.085 52)",
      "--wallet-lining": "oklch(0.36 0.07 46)",
      "--wallet-edge": "oklch(0.3 0.06 44)",
      "--wallet-recess": "oklch(0.29 0.055 44)",
      "--wallet-foreground": "oklch(0.97 0.02 82)",
      "--wallet-muted-foreground": "oklch(0.88 0.035 72)",
      "--wallet-stitch": "oklch(0.88 0.05 82 / 0.8)",
      "--wallet-meter": "oklch(0.84 0.12 85)",
      "--wallet-ok": "oklch(0.82 0.18 150)",
      "--wallet-low": "oklch(0.86 0.16 82)",
      "--wallet-danger": "oklch(0.72 0.19 28)",
      "--wallet-notch": "1.75rem",
      "--wallet-card-peek": "2.25rem",
      "--wallet-card-pull": "0.5rem",
      "--wallet-card-enter": "3rem",
      "--wallet-card-step": "1.25rem",
      "--wallet-card-step-pick": "2rem",
      "--wallet-card-lift": "0px",
      "--wallet-card-slots": "3"
    },
    "[data-slot=\"wallet\"][data-variant=\"noir\"]": {
      "--wallet-leather": "oklch(0.29 0.008 260)",
      "--wallet-lining": "oklch(0.22 0.008 260)",
      "--wallet-edge": "oklch(0.16 0.006 260)",
      "--wallet-recess": "oklch(0.17 0.006 260)",
      "--wallet-foreground": "oklch(0.96 0.004 260)",
      "--wallet-muted-foreground": "oklch(0.8 0.008 260)",
      "--wallet-stitch": "oklch(0.72 0.01 260 / 0.75)",
      "--wallet-meter": "oklch(0.88 0.01 260)"
    },
    "[data-slot=\"wallet\"][data-variant=\"oxblood\"]": {
      "--wallet-leather": "oklch(0.38 0.105 22)",
      "--wallet-lining": "oklch(0.29 0.085 20)",
      "--wallet-edge": "oklch(0.23 0.07 20)",
      "--wallet-recess": "oklch(0.22 0.065 20)",
      "--wallet-foreground": "oklch(0.96 0.02 70)",
      "--wallet-muted-foreground": "oklch(0.87 0.035 50)",
      "--wallet-stitch": "oklch(0.86 0.05 75 / 0.75)"
    },
    "[data-slot=\"wallet\"]": {
      "--bevel-material": "var(--wallet-lining)",
      "--bevel-material-edge": "var(--wallet-edge)",
      "--bevel-material-recess": "var(--wallet-recess)",
      "--bevel-stitch": "var(--wallet-stitch)",
      "--bevel-fill": "var(--wallet-meter)",
      "--bevel-led": "var(--wallet-ok)"
    },
    "[data-slot=\"wallet-pocket\"]": {
      "--bevel-material": "var(--wallet-leather)"
    },
    "[data-slot=\"wallet\"][data-status=\"low\"]": {
      "--bevel-fill": "var(--wallet-low)",
      "--bevel-led": "var(--wallet-low)"
    },
    "[data-slot=\"wallet\"]:is([data-status=\"empty\"], [data-status=\"insufficient\"])": {
      "--bevel-fill": "var(--wallet-danger)",
      "--bevel-led": "var(--wallet-danger)"
    },
    "@utility wallet-pocket": {
      "background-color": "var(--bevel-material)",
      "background-image": "radial-gradient( circle at 50% 0, transparent var(--wallet-notch), var(--bevel-highlight) calc(var(--wallet-notch) + 1px), transparent calc(var(--wallet-notch) + 4px) ), var(--bevel-grain), linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade))",
      "background-size": "100% 100%, var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%",
      "background-blend-mode": "normal, soft-light, normal",
      "box-shadow": "inset 0 1px 0 var(--bevel-highlight), inset 0 2px 0 var(--bevel-shade), inset 0 -2px 3px var(--bevel-shade)",
      "mask-image": "radial-gradient(circle at 50% 0, transparent var(--wallet-notch), black calc(var(--wallet-notch) + 0.5px))"
    },
    "@layer components": {
      "[data-slot=\"wallet-cards\"]": {
        "--wallet-card-gap": "calc( var(--wallet-card-step) * (var(--wallet-card-slots) - 1) / max(var(--wallet-card-count) - 1, var(--wallet-card-slots) - 1) )",
        "height": "calc(var(--wallet-card-peek) + (var(--wallet-card-slots) - 1) * var(--wallet-card-step))"
      },
      "[data-slot=\"wallet-cards\"][data-selectable]": {
        "--wallet-card-gap": "var(--wallet-card-step-pick)",
        "height": "calc(var(--wallet-card-peek) + max(var(--wallet-card-count) - 1, 0) * var(--wallet-card-step-pick))"
      }
    }
  },
  "categories": [
    "money"
  ],
  "type": "registry:ui"
}