{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "payment-method-card",
  "title": "Payment method card",
  "description": "Payment method card is a production-ready cards component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/cards/payment-method-card.tsx",
      "content": "\"use client\";\n\nimport {\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { Check, CreditCard, Plus, RefreshCcw, Trash2 } from \"lucide-react\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport { ease, springs } from \"@/components/bjork-ui/_core/motion\";\nimport {\n  CardButton,\n  CardFrame,\n  Pill,\n  focusRing,\n  useCardTheme,\n  useClock,\n  useReducedMotionSafe,\n  type CardTheme,\n} from \"@/components/bjork-ui/cards/card-kit\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CardFinish = \"graphite\" | \"ember\" | \"sand\" | \"ink\";\n\nexport interface PaymentMethod {\n  id: string;\n  /** Network label, e.g. \"Orbit\". Drawn as a neutral two-ring mark. */\n  network: string;\n  /** Issuer wordmark on the front of the card. */\n  issuer: string;\n  last4: string;\n  holder: string;\n  /** 1–12. */\n  expMonth: number;\n  /** Four-digit year. */\n  expYear: number;\n  finish?: CardFinish;\n  billingPostal?: string;\n  /** ISO date the card was added. */\n  addedOn?: string;\n}\n\nexport interface PaymentMethodCardProps {\n  /** Starting list. The card manages selection, default and removal itself and reports each change. */\n  defaultMethods?: PaymentMethod[];\n  defaultDefaultId?: string;\n  onDefaultChange?: (id: string) => void;\n  onRemove?: (id: string) => void;\n  onAdd?: () => void;\n  title?: string;\n  /** Epoch ms. Freezes the clock for previews and tests. */\n  now?: number;\n  locale?: string;\n  theme?: CardTheme;\n  className?: string;\n}\n\nexport const PAYMENT_METHOD_SAMPLE: PaymentMethod[] = [\n  {\n    id: \"pm_orbit_4821\",\n    network: \"Orbit\",\n    issuer: \"Halden\",\n    last4: \"4821\",\n    holder: \"Mara Okonjo\",\n    expMonth: 8,\n    expYear: 2028,\n    finish: \"graphite\",\n    billingPostal: \"94110\",\n    addedOn: \"2025-03-12\",\n  },\n  {\n    id: \"pm_lumen_0937\",\n    network: \"Lumen\",\n    issuer: \"Cobalt Credit Union\",\n    last4: \"0937\",\n    holder: \"Mara Okonjo\",\n    expMonth: 11,\n    expYear: 2026,\n    finish: \"ember\",\n    billingPostal: \"94110\",\n    addedOn: \"2023-11-02\",\n  },\n  {\n    id: \"pm_orbit_5502\",\n    network: \"Orbit\",\n    issuer: \"Northwind\",\n    last4: \"5502\",\n    holder: \"Mara Okonjo\",\n    expMonth: 4,\n    expYear: 2026,\n    finish: \"sand\",\n    billingPostal: \"10013\",\n    addedOn: \"2022-04-19\",\n  },\n];\n\nconst FINISHES: Record<CardFinish, { bg: string; ink: string; soft: string; chip: string }> = {\n  graphite: {\n    bg: \"linear-gradient(135deg, #2b2b2e 0%, #18181a 55%, #0f0f10 100%)\",\n    ink: \"#f2f0ec\",\n    soft: \"rgba(242,240,236,0.6)\",\n    chip: \"linear-gradient(135deg,#d9c89f,#a68f5f)\",\n  },\n  ember: {\n    bg: \"linear-gradient(135deg, #f07a3a 0%, #c4470f 60%, #8f310a 100%)\",\n    ink: \"#fff6ef\",\n    soft: \"rgba(255,246,239,0.72)\",\n    chip: \"linear-gradient(135deg,#f5dfb1,#c9a565)\",\n  },\n  sand: {\n    bg: \"linear-gradient(135deg, #efe6d6 0%, #dccdb2 60%, #c7b391 100%)\",\n    ink: \"#2a2218\",\n    soft: \"rgba(42,34,24,0.62)\",\n    chip: \"linear-gradient(135deg,#cfb27a,#9c7f4a)\",\n  },\n  ink: {\n    bg: \"linear-gradient(135deg, #24324a 0%, #141c2b 60%, #0b1019 100%)\",\n    ink: \"#eef2fa\",\n    soft: \"rgba(238,242,250,0.62)\",\n    chip: \"linear-gradient(135deg,#d9c89f,#a68f5f)\",\n  },\n};\n\ntype ExpiryState = \"valid\" | \"soon\" | \"expired\";\n\nfunction expiryState(m: PaymentMethod, now: number | null): ExpiryState {\n  if (now === null) return \"valid\";\n  const d = new Date(now);\n  const months = (m.expYear - d.getUTCFullYear()) * 12 + (m.expMonth - (d.getUTCMonth() + 1));\n  if (months < 0) return \"expired\";\n  if (months <= 2) return \"soon\";\n  return \"valid\";\n}\n\nconst pad2 = (n: number) => String(n).padStart(2, \"0\");\n\n/**\n * Saved payment methods. A tilting card face shows the selected method and flips for billing details;\n * the list below picks, defaults and removes cards, with expiry warnings.\n */\nexport function PaymentMethodCard({\n  defaultMethods = PAYMENT_METHOD_SAMPLE,\n  defaultDefaultId,\n  onDefaultChange,\n  onRemove,\n  onAdd,\n  title = \"Payment methods\",\n  now,\n  locale = \"en-US\",\n  theme = \"auto\",\n  className,\n}: PaymentMethodCardProps) {\n  const { style } = useCardTheme(theme);\n  const reduce = useReducedMotionSafe();\n  const clock = useClock(now, 3_600_000);\n  const titleId = useId();\n  const listId = useId();\n\n  const [methods, setMethods] = useState(defaultMethods);\n  const [defaultId, setDefaultId] = useState(defaultDefaultId ?? defaultMethods[0]?.id ?? \"\");\n  const [selectedId, setSelectedId] = useState(defaultDefaultId ?? defaultMethods[0]?.id ?? \"\");\n  const [flipped, setFlipped] = useState(false);\n  const [confirming, setConfirming] = useState(false);\n  const [message, setMessage] = useState(\"\");\n  const optionRefs = useRef(new Map<string, HTMLDivElement>());\n\n  const selected = methods.find((m) => m.id === selectedId) ?? methods[0];\n  const label = (m: PaymentMethod) => `${m.network} ending ${m.last4}`;\n\n  const select = (id: string, focus = false) => {\n    setSelectedId(id);\n    setFlipped(false);\n    setConfirming(false);\n    if (focus) optionRefs.current.get(id)?.focus();\n  };\n\n  const onListKey = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    const i = methods.findIndex((m) => m.id === selected?.id);\n    let next = -1;\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") next = (i + 1) % methods.length;\n    if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") next = (i - 1 + methods.length) % methods.length;\n    if (e.key === \"Home\") next = 0;\n    if (e.key === \"End\") next = methods.length - 1;\n    if (next >= 0) {\n      e.preventDefault();\n      select(methods[next].id, true);\n    }\n  };\n\n  const makeDefault = () => {\n    if (!selected) return;\n    setDefaultId(selected.id);\n    onDefaultChange?.(selected.id);\n    setMessage(`${label(selected)} is now your default payment method`);\n  };\n\n  const remove = () => {\n    if (!selected) return;\n    const i = methods.findIndex((m) => m.id === selected.id);\n    const rest = methods.filter((m) => m.id !== selected.id);\n    setMethods(rest);\n    onRemove?.(selected.id);\n    setConfirming(false);\n    setMessage(`Removed ${label(selected)}`);\n    const next = rest[Math.min(i, rest.length - 1)];\n    if (next) {\n      setSelectedId(next.id);\n      requestAnimationFrame(() => optionRefs.current.get(next.id)?.focus());\n    }\n  };\n\n  const fmtAdded = useMemo(\n    () => new Intl.DateTimeFormat(locale, { month: \"short\", day: \"numeric\", year: \"numeric\", timeZone: \"UTC\" }),\n    [locale],\n  );\n\n  return (\n    <CardFrame aria-labelledby={titleId} className={className} style={style}>\n      <header className=\"flex items-center justify-between gap-3 px-5 pt-5\">\n        <h3 id={titleId} className=\"text-[15px] font-semibold leading-5\">\n          {title}\n        </h3>\n        {onAdd && methods.length > 0 && (\n          <CardButton size=\"sm\" variant=\"ghost\" onClick={onAdd} icon={<Plus aria-hidden=\"true\" className=\"size-3.5\" />} className=\"-mr-1.5\">\n            Add\n          </CardButton>\n        )}\n      </header>\n\n      {methods.length === 0 || !selected ? (\n        <div className=\"px-5 pb-5 pt-4\">\n          <div className=\"grid aspect-[1.586] w-full place-items-center rounded-[14px] border border-dashed border-[color:var(--bjork-border-strong)] bg-[color:var(--bjork-card-inset)] p-6 text-center\">\n            <div>\n              <CreditCard aria-hidden=\"true\" className=\"mx-auto size-6 text-[color:var(--bjork-text-soft)]\" />\n              <p className=\"mt-2 text-[13px] font-medium\">No saved payment method</p>\n              <p className=\"mt-1 text-[12px] text-[color:var(--bjork-text-muted)]\">Add a card to keep your subscription active.</p>\n              {onAdd && (\n                <CardButton variant=\"primary\" size=\"sm\" onClick={onAdd} className=\"mt-4\" icon={<Plus aria-hidden=\"true\" className=\"size-3.5\" />}>\n                  Add payment method\n                </CardButton>\n              )}\n            </div>\n          </div>\n        </div>\n      ) : (\n        <>\n          <div className=\"px-5 pt-4\">\n            <CardFace\n              method={selected}\n              flipped={flipped}\n              reduce={reduce}\n              addedLabel={selected.addedOn ? fmtAdded.format(Date.parse(selected.addedOn)) : undefined}\n            />\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2 px-5 pt-3\">\n            <CardButton\n              size=\"sm\"\n              aria-pressed={flipped}\n              onClick={() => setFlipped((f) => !f)}\n              icon={<RefreshCcw aria-hidden=\"true\" className=\"size-3.5\" />}\n            >\n              {flipped ? \"Show front\" : \"Billing details\"}\n            </CardButton>\n            <CardButton\n              size=\"sm\"\n              onClick={makeDefault}\n              disabled={selected.id === defaultId || expiryState(selected, clock) === \"expired\"}\n              icon={<Check aria-hidden=\"true\" className=\"size-3.5\" />}\n            >\n              {selected.id === defaultId ? \"Default\" : \"Make default\"}\n            </CardButton>\n            <CardButton\n              size=\"sm\"\n              variant=\"ghost\"\n              onClick={() => setConfirming(true)}\n              disabled={selected.id === defaultId && methods.length > 1}\n              title={selected.id === defaultId && methods.length > 1 ? \"Make another card the default first\" : undefined}\n              icon={<Trash2 aria-hidden=\"true\" className=\"size-3.5\" />}\n              className=\"ml-auto\"\n            >\n              Remove\n            </CardButton>\n          </div>\n\n          <AnimatePresence initial={false}>\n            {confirming && (\n              <motion.div\n                key=\"confirm\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n                animate={reduce ? { opacity: 1 } : { opacity: 1, height: \"auto\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n                transition={{ duration: 0.22, ease: ease.out }}\n                className=\"overflow-hidden\"\n              >\n                <div\n                  role=\"alertdialog\"\n                  aria-label={`Remove ${label(selected)}?`}\n                  className=\"mx-5 mt-3 flex flex-wrap items-center justify-between gap-2 rounded-[12px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-error-soft)] px-3 py-2.5\"\n                >\n                  <p className=\"text-[12px] leading-4 text-[color:var(--bjork-text-medium)]\">\n                    Remove <span className=\"font-medium text-[color:var(--bjork-text)]\">•••• {selected.last4}</span>?\n                  </p>\n                  <div className=\"flex gap-1.5\">\n                    <CardButton size=\"sm\" variant=\"ghost\" onClick={() => setConfirming(false)} autoFocus>\n                      Cancel\n                    </CardButton>\n                    <CardButton size=\"sm\" variant=\"danger\" onClick={remove}>\n                      Remove\n                    </CardButton>\n                  </div>\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          <div\n            id={listId}\n            role=\"radiogroup\"\n            aria-label=\"Saved cards\"\n            onKeyDown={onListKey}\n            className=\"mt-4 border-t border-[color:var(--bjork-border)] px-2 py-2\"\n          >\n            {methods.map((m) => {\n              const exp = expiryState(m, clock);\n              const isSel = m.id === selected.id;\n              return (\n                <div\n                  key={m.id}\n                  ref={(el) => {\n                    if (el) optionRefs.current.set(m.id, el);\n                    else optionRefs.current.delete(m.id);\n                  }}\n                  role=\"radio\"\n                  aria-checked={isSel}\n                  tabIndex={isSel ? 0 : -1}\n                  onClick={() => select(m.id)}\n                  onKeyDown={(e) => {\n                    if (e.key === \" \" || e.key === \"Enter\") {\n                      e.preventDefault();\n                      select(m.id);\n                    }\n                  }}\n                  className={cn(\n                    \"relative flex cursor-pointer items-center gap-3 rounded-[12px] px-3 py-2.5 transition-colors duration-150\",\n                    isSel ? \"bg-[color:var(--bjork-card-raised)]\" : \"hover:bg-[color:var(--bjork-card-hover)]\",\n                    focusRing,\n                  )}\n                >\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"h-6 w-9 shrink-0 rounded-[5px] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.12)]\"\n                    style={{ background: FINISHES[m.finish ?? \"graphite\"].bg }}\n                  />\n                  <span className=\"min-w-0 flex-1\">\n                    <span className=\"block truncate text-[13px] font-medium leading-5\">\n                      {m.network} •••• {m.last4}\n                    </span>\n                    <span\n                      className={cn(\n                        \"block text-[12px] leading-4\",\n                        exp === \"expired\"\n                          ? \"text-[color:var(--bjork-error)]\"\n                          : exp === \"soon\"\n                            ? \"text-[color:var(--bjork-warning)]\"\n                            : \"text-[color:var(--bjork-text-muted)]\",\n                      )}\n                    >\n                      {exp === \"expired\" ? \"Expired\" : \"Expires\"} {pad2(m.expMonth)}/{String(m.expYear).slice(-2)}\n                    </span>\n                  </span>\n                  {m.id === defaultId && <Pill tone=\"accent\">Default</Pill>}\n                  {exp === \"expired\" && <Pill tone=\"error\">Expired</Pill>}\n                  {exp === \"soon\" && m.id !== defaultId && <Pill tone=\"warning\">Expires soon</Pill>}\n                  <span\n                    aria-hidden=\"true\"\n                    className={cn(\n                      \"grid size-4 shrink-0 place-items-center rounded-full border transition-colors\",\n                      isSel\n                        ? \"border-[color:var(--bjork-accent)] bg-[color:var(--bjork-accent)]\"\n                        : \"border-[color:var(--bjork-border-strong)]\",\n                    )}\n                  >\n                    {isSel && <span className=\"size-1.5 rounded-full bg-[color:var(--bjork-card)]\" />}\n                  </span>\n                </div>\n              );\n            })}\n          </div>\n        </>\n      )}\n      <LiveRegion message={message} />\n    </CardFrame>\n  );\n}\n\nfunction NetworkMark({ color }: { color: string }) {\n  return (\n    <svg width=\"34\" height=\"22\" viewBox=\"0 0 34 22\" aria-hidden=\"true\">\n      <circle cx=\"12\" cy=\"11\" r=\"9\" fill=\"none\" stroke={color} strokeWidth=\"1.6\" />\n      <circle cx=\"22\" cy=\"11\" r=\"9\" fill=\"none\" stroke={color} strokeWidth=\"1.6\" opacity=\"0.65\" />\n    </svg>\n  );\n}\n\nfunction CardFace({\n  method,\n  flipped,\n  reduce,\n  addedLabel,\n}: {\n  method: PaymentMethod;\n  flipped: boolean;\n  reduce: boolean;\n  addedLabel?: string;\n}) {\n  const f = FINISHES[method.finish ?? \"graphite\"];\n  const [tilt, setTilt] = useState({ x: 0, y: 0, gx: 50, gy: 30 });\n\n  const onMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n    if (reduce || e.pointerType !== \"mouse\") return;\n    const r = e.currentTarget.getBoundingClientRect();\n    const px = (e.clientX - r.left) / r.width;\n    const py = (e.clientY - r.top) / r.height;\n    setTilt({ x: (0.5 - py) * 8, y: (px - 0.5) * 10, gx: px * 100, gy: py * 100 });\n  };\n  const onLeave = () => setTilt({ x: 0, y: 0, gx: 50, gy: 30 });\n\n  const face: CSSProperties = {\n    background: f.bg,\n    color: f.ink,\n    backfaceVisibility: \"hidden\",\n    WebkitBackfaceVisibility: \"hidden\",\n  };\n  const sheen = (\n    <span\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 rounded-[inherit] mix-blend-soft-light\"\n      style={{\n        background: `radial-gradient(120% 90% at ${tilt.gx}% ${tilt.gy}%, rgba(255,255,255,0.35), transparent 55%)`,\n      }}\n    />\n  );\n  const exp = `${pad2(method.expMonth)}/${String(method.expYear).slice(-2)}`;\n\n  return (\n    <div\n      className=\"relative w-full [perspective:1100px]\"\n      onPointerMove={onMove}\n      onPointerLeave={onLeave}\n      role=\"group\"\n      aria-roledescription=\"payment card\"\n      aria-label={`${method.issuer} ${method.network} card ending ${method.last4}, ${method.holder}, expires ${exp}`}\n    >\n      <motion.div\n        className=\"relative aspect-[1.586] w-full [transform-style:preserve-3d]\"\n        animate={{ rotateX: tilt.x, rotateY: (reduce ? 0 : flipped ? 180 : 0) + tilt.y }}\n        transition={reduce ? { duration: 0 } : springs.soft}\n      >\n        {/* Front */}\n        <motion.div\n          aria-hidden={flipped}\n          className=\"absolute inset-0 flex flex-col justify-between overflow-hidden rounded-[14px] p-[6%] shadow-[0_18px_30px_-18px_rgba(0,0,0,0.6),inset_0_0_0_1px_rgba(255,255,255,0.08)]\"\n          style={face}\n          animate={reduce ? { opacity: flipped ? 0 : 1 } : undefined}\n          transition={{ duration: 0.18 }}\n        >\n          {sheen}\n          <div className=\"relative flex items-start justify-between\">\n            <span className=\"text-[13px] font-semibold tracking-[0.02em]\">{method.issuer}</span>\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" style={{ color: f.soft }}>\n              <path d=\"M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n            </svg>\n          </div>\n          <span\n            aria-hidden=\"true\"\n            className=\"relative block h-[22%] w-[13%] rounded-[5px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.18)]\"\n            style={{ background: f.chip }}\n          />\n          <div className=\"relative\">\n            <p className=\"font-mono text-[clamp(13px,4.6cqw,17px)] tracking-[0.14em]\">\n              •••• •••• •••• {method.last4}\n            </p>\n            <div className=\"mt-[3%] flex items-end justify-between gap-3\">\n              <div className=\"min-w-0\">\n                <p className=\"text-[11px] uppercase tracking-[0.14em]\" style={{ color: f.soft }}>\n                  Card holder\n                </p>\n                <p className=\"truncate text-[12px] font-medium uppercase tracking-[0.06em]\">{method.holder}</p>\n              </div>\n              <div className=\"shrink-0\">\n                <p className=\"text-[11px] uppercase tracking-[0.14em]\" style={{ color: f.soft }}>\n                  Expires\n                </p>\n                <p className=\"text-[12px] font-medium\">{exp}</p>\n              </div>\n              <div className=\"flex shrink-0 flex-col items-center\">\n                <NetworkMark color={f.ink} />\n                <span className=\"text-[11px] uppercase tracking-[0.18em]\" style={{ color: f.soft }}>\n                  {method.network}\n                </span>\n              </div>\n            </div>\n          </div>\n        </motion.div>\n\n        {/* Back */}\n        <motion.div\n          aria-hidden={!flipped}\n          className=\"absolute inset-0 flex flex-col overflow-hidden rounded-[14px] shadow-[0_18px_30px_-18px_rgba(0,0,0,0.6),inset_0_0_0_1px_rgba(255,255,255,0.08)]\"\n          style={{ ...face, transform: reduce ? undefined : \"rotateY(180deg)\" }}\n          initial={false}\n          animate={reduce ? { opacity: flipped ? 1 : 0 } : { opacity: 1 }}\n          transition={{ duration: 0.18 }}\n        >\n          {sheen}\n          <span aria-hidden=\"true\" className=\"mt-[8%] block h-[18%] w-full bg-black/70\" />\n          <div className=\"relative flex flex-1 flex-col justify-between p-[6%] pt-[5%]\">\n            <div className=\"flex items-center gap-2\">\n              <span className=\"h-7 flex-1 rounded-[4px] bg-[repeating-linear-gradient(90deg,rgba(255,255,255,0.75)_0_6px,rgba(255,255,255,0.6)_6px_12px)]\" />\n              <span className=\"rounded-[4px] bg-white/85 px-2 py-1 font-mono text-[11px] text-black/70\">CVC •••</span>\n            </div>\n            <dl className=\"grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]\">\n              <div>\n                <dt style={{ color: f.soft }}>Billing postal code</dt>\n                <dd className=\"font-medium\">{method.billingPostal ?? \"—\"}</dd>\n              </div>\n              <div>\n                <dt style={{ color: f.soft }}>Added</dt>\n                <dd className=\"font-medium\">{addedLabel ?? \"—\"}</dd>\n              </div>\n              <div className=\"col-span-2\">\n                <dt style={{ color: f.soft }}>Issued by</dt>\n                <dd className=\"font-medium\">{method.issuer}</dd>\n              </div>\n            </dl>\n          </div>\n        </motion.div>\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/cards/payment-method-card.tsx"
    },
    {
      "path": "components/bjork-ui/_core/a11y.tsx",
      "content": "import type { ReactNode } from \"react\";\n\n// Text for assistive tech only.\nexport function VisuallyHidden({ children }: { children: ReactNode }) {\n  return <span className=\"sr-only\">{children}</span>;\n}\n\n// Announces `message` when it changes. Same text re-rendered does not re-announce.\nexport function LiveRegion({\n  message,\n  politeness = \"polite\",\n}: {\n  message: string;\n  politeness?: \"polite\" | \"assertive\";\n}) {\n  return (\n    <span role=\"status\" aria-live={politeness} aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/a11y.tsx"
    },
    {
      "path": "components/bjork-ui/_core/motion.ts",
      "content": "// Shared springs, easings and durations. Components import these instead of inventing values.\n\nexport const springs = {\n  press: { type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 },\n  snappy: { type: \"spring\", stiffness: 400, damping: 25, mass: 1 },\n  standard: { type: \"spring\", stiffness: 300, damping: 30, mass: 0.8 },\n  soft: { type: \"spring\", stiffness: 200, damping: 25, mass: 1.2 },\n  settle: { type: \"spring\", stiffness: 380, damping: 30, mass: 0.8 },\n  blurIn: { type: \"spring\", stiffness: 400, damping: 28, mass: 0.6 },\n} as const;\n\nexport const ease = {\n  out: [0.23, 1, 0.32, 1],\n  inOut: [0.77, 0, 0.175, 1],\n  drawer: [0.32, 0.72, 0, 1],\n} as const;\n\nexport const easeCss = {\n  out: \"cubic-bezier(0.23,1,0.32,1)\",\n  inOut: \"cubic-bezier(0.77,0,0.175,1)\",\n  drawer: \"cubic-bezier(0.32,0.72,0,1)\",\n} as const;\n\nexport const durations = { press: 140, tooltip: 160, menu: 200, panel: 280 } as const; // ms\n\n// Cubic bezier easing for canvas and rAF code. Returns progress y for input x in [0, 1].\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (s: number) => ((ax * s + bx) * s + cx) * s;\n  const sampleY = (s: number) => ((ay * s + by) * s + cy) * s;\n  const sampleDX = (s: number) => (3 * ax * s + 2 * bx) * s + cx;\n\n  const solveX = (x: number) => {\n    let s = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(s) - x;\n      if (Math.abs(err) < 1e-6) return s;\n      const d = sampleDX(s);\n      if (Math.abs(d) < 1e-6) break;\n      s -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    s = x;\n    while (lo < hi) {\n      const v = sampleX(s);\n      if (Math.abs(v - x) < 1e-6) return s;\n      if (x > v) lo = s;\n      else hi = s;\n      s = (lo + hi) / 2;\n      if (hi - lo < 1e-7) break;\n    }\n    return s;\n  };\n\n  return (t: number) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    return sampleY(solveX(t));\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/motion.ts"
    },
    {
      "path": "components/bjork-ui/cards/card-kit.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useSyncExternalStore,\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  type HTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport type { BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { surfaceVars } from \"@/components/bjork-ui/_core/surface\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { cn } from \"@/lib/utils\";\n\n/** \"auto\" follows the site theme (next-themes). \"light\" and \"dark\" pin the card. */\nexport type CardTheme = \"light\" | \"dark\" | \"auto\";\n\n/**\n * Card tokens. They are written onto the card root as the library's `--bjork-*` custom properties,\n * so a pinned theme wins over the page theme, and the status colours the global sheet lacks\n * (success, warning, error) are always defined.\n */\nexport const CARD_TOKENS = {\n  dark: {\n    card: \"#141414\",\n    raised: \"#1b1b1b\",\n    inset: \"#0d0d0d\",\n    hover: \"#1f1f1f\",\n    border: \"#242424\",\n    borderStrong: \"#363636\",\n    hair: \"rgba(237,237,237,0.08)\",\n    text: \"#ededed\",\n    textMedium: \"rgba(237,237,237,0.74)\",\n    textMuted: \"rgba(237,237,237,0.56)\",\n    textSoft: \"rgba(237,237,237,0.4)\",\n    textFaint: \"rgba(237,237,237,0.22)\",\n    accent: \"#ec5c13\",\n    accentInk: \"#f27a3c\",\n    accentFill: \"#b84a12\",\n    accentFillHover: \"#c95517\",\n    accentFg: \"#fff2ea\",\n    accentSoft: \"rgba(236,92,19,0.13)\",\n    accentMuted: \"rgba(236,92,19,0.26)\",\n    success: \"#4cc38a\",\n    successSoft: \"rgba(76,195,138,0.13)\",\n    warning: \"#f2b544\",\n    warningSoft: \"rgba(242,181,68,0.13)\",\n    error: \"#ff6b5c\",\n    errorSoft: \"rgba(255,107,92,0.13)\",\n    track: \"#262626\",\n    shadow:\n      \"inset 0 1px 0 rgba(255,255,255,0.045), 0 1px 0 rgba(0,0,0,0.5), 0 24px 48px -28px rgba(0,0,0,0.9)\",\n    raisedShadow: \"inset 0 1px 0 rgba(255,255,255,0.04)\",\n  },\n  light: {\n    card: \"#fffcf6\",\n    raised: \"#f7f1e6\",\n    inset: \"#f1eadd\",\n    hover: \"#f3ecdf\",\n    border: \"#ebe2d4\",\n    borderStrong: \"#dbcfbd\",\n    hair: \"rgba(23,23,23,0.07)\",\n    text: \"#171717\",\n    textMedium: \"rgba(23,23,23,0.76)\",\n    textMuted: \"rgba(23,23,23,0.62)\",\n    textSoft: \"rgba(23,23,23,0.46)\",\n    textFaint: \"rgba(23,23,23,0.2)\",\n    accent: \"#ec7d43\",\n    accentInk: \"#a84a19\",\n    accentFill: \"#ec7d43\",\n    accentFillHover: \"#f0935f\",\n    accentFg: \"#3f2112\",\n    accentSoft: \"rgba(236,125,67,0.12)\",\n    accentMuted: \"rgba(236,125,67,0.24)\",\n    success: \"#1b7f4d\",\n    successSoft: \"rgba(31,138,85,0.11)\",\n    warning: \"#966000\",\n    warningSoft: \"rgba(168,107,0,0.11)\",\n    error: \"#c0331d\",\n    errorSoft: \"rgba(200,54,31,0.1)\",\n    track: \"#ebe3d6\",\n    shadow:\n      \"inset 0 1px 0 rgba(255,255,255,0.9), 0 1px 2px rgba(66,52,33,0.06), 0 22px 44px -28px rgba(66,52,33,0.26)\",\n    raisedShadow: \"inset 0 1px 0 rgba(255,255,255,0.7)\",\n  },\n} as const;\n\nexport type CardTokens = (typeof CARD_TOKENS)[BjorkTone];\n\nexport function cardVars(tone: BjorkTone): CSSProperties {\n  const t = CARD_TOKENS[tone];\n  return {\n    colorScheme: tone,\n    ...surfaceVars(tone),\n    \"--bjork-card\": t.card,\n    \"--bjork-card-raised\": t.raised,\n    \"--bjork-card-inset\": t.inset,\n    \"--bjork-card-hover\": t.hover,\n    \"--bjork-border\": t.border,\n    \"--bjork-border-strong\": t.borderStrong,\n    \"--bjork-hair\": t.hair,\n    \"--bjork-text\": t.text,\n    \"--bjork-text-medium\": t.textMedium,\n    \"--bjork-text-muted\": t.textMuted,\n    \"--bjork-text-soft\": t.textSoft,\n    \"--bjork-text-faint\": t.textFaint,\n    \"--bjork-accent\": t.accent,\n    \"--bjork-accent-ink\": t.accentInk,\n    \"--bjork-accent-fill\": t.accentFill,\n    \"--bjork-accent-fill-hover\": t.accentFillHover,\n    \"--bjork-accent-foreground\": t.accentFg,\n    \"--bjork-accent-soft\": t.accentSoft,\n    \"--bjork-accent-muted\": t.accentMuted,\n    \"--bjork-success\": t.success,\n    \"--bjork-success-soft\": t.successSoft,\n    \"--bjork-warning\": t.warning,\n    \"--bjork-warning-soft\": t.warningSoft,\n    \"--bjork-error\": t.error,\n    \"--bjork-error-soft\": t.errorSoft,\n    \"--bjork-track\": t.track,\n    \"--bjork-card-shadow\": t.shadow,\n    \"--bjork-raised-shadow\": t.raisedShadow,\n    \"--bjork-ring-offset\": t.card,\n  } as CSSProperties;\n}\n\n/** Resolves the card tone and the CSS variables for it. */\nexport function useCardTheme(theme: CardTheme = \"auto\") {\n  const tone = useBjorkTone(theme === \"auto\" ? undefined : theme);\n  return { tone, tokens: CARD_TOKENS[tone], style: cardVars(tone) };\n}\n\nconst noopSubscribe = () => () => {};\n\n/** True after hydration. The server render and the first client render both see false. */\nexport function useHydrated() {\n  return useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false,\n  );\n}\n\n/** Reduced-motion preference, hydration-safe: false until mounted. */\nexport function useReducedMotionSafe() {\n  const hydrated = useHydrated();\n  const reduced = useReducedMotion();\n  return hydrated && reduced === true;\n}\n\nconst clockStores = new Map<number, { subscribe: (cb: () => void) => () => void; get: () => number }>();\n\nfunction clockStore(stepMs: number) {\n  let store = clockStores.get(stepMs);\n  if (!store) {\n    store = {\n      subscribe: (cb) => {\n        const id = window.setInterval(cb, Math.min(stepMs, 1000));\n        return () => window.clearInterval(id);\n      },\n      get: () => Math.floor(Date.now() / stepMs) * stepMs,\n    };\n    clockStores.set(stepMs, store);\n  }\n  return store;\n}\n\n/**\n * Wall clock rounded to `stepMs`. A fixed `now` (previews, tests) wins. Returns null during SSR and\n * hydration so server and client text agree; callers render a neutral fallback for that frame.\n */\nexport function useClock(now: number | undefined, stepMs = 60_000): number | null {\n  const store = clockStore(stepMs);\n  const live = useSyncExternalStore(store.subscribe, store.get, () => null);\n  return now ?? live;\n}\n\nexport const focusRing =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)]\";\n\nexport interface CardFrameProps extends HTMLAttributes<HTMLElement> {\n  style?: CSSProperties;\n  /** Width in px, capped at the container width. Default 420. */\n  maxWidth?: number;\n  as?: \"section\" | \"article\" | \"div\";\n}\n\n/** The shared card surface: tokens, radius, border, shadow and a container query root. */\nexport const CardFrame = forwardRef<HTMLElement, CardFrameProps>(function CardFrame(\n  { as: Tag = \"section\", maxWidth = 420, className, style, children, ...rest },\n  ref,\n) {\n  return (\n    <Tag\n      ref={ref as never}\n      className={cn(\n        \"@container relative max-w-full overflow-hidden rounded-[20px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-card)] font-bjork-alpha text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)] [font-variant-numeric:tabular-nums]\",\n        className,\n      )}\n      // A fixed width capped at the container: fills narrow columns, and keeps its size inside\n      // shrink-to-fit parents (flex centring, popovers) where a percentage width would collapse.\n      style={{ width: maxWidth, ...style }}\n      {...rest}\n    >\n      {children}\n    </Tag>\n  );\n});\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n\nexport interface CardButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant?: ButtonVariant;\n  size?: \"sm\" | \"md\";\n  icon?: ReactNode;\n}\n\nconst buttonVariants: Record<ButtonVariant, string> = {\n  primary:\n    \"bg-[color:var(--bjork-accent-fill)] text-[color:var(--bjork-accent-foreground)] hover:bg-[color:var(--bjork-accent-fill-hover)] shadow-[inset_0_1px_0_rgba(255,255,255,0.16)]\",\n  secondary:\n    \"border border-[color:var(--bjork-border)] bg-[color:var(--bjork-card-raised)] text-[color:var(--bjork-text)] shadow-[var(--bjork-raised-shadow)] hover:bg-[color:var(--bjork-card-hover)] hover:border-[color:var(--bjork-border-strong)]\",\n  ghost:\n    \"text-[color:var(--bjork-text-medium)] hover:bg-[color:var(--bjork-card-hover)] hover:text-[color:var(--bjork-text)]\",\n  danger:\n    \"border border-[color:var(--bjork-border)] bg-[color:var(--bjork-error-soft)] text-[color:var(--bjork-error)] hover:border-[color:var(--bjork-error)]\",\n};\n\nexport const CardButton = forwardRef<HTMLButtonElement, CardButtonProps>(function CardButton(\n  { variant = \"secondary\", size = \"md\", icon, className, children, type = \"button\", ...rest },\n  ref,\n) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      className={cn(\n        \"inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-[10px] font-medium transition-[background-color,border-color,color,transform,opacity] duration-150 ease-out active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-45 disabled:active:scale-100 motion-reduce:transition-none motion-reduce:active:scale-100\",\n        size === \"sm\" ? \"h-8 px-2.5 text-[12px]\" : \"h-9 px-3.5 text-[13px]\",\n        buttonVariants[variant],\n        focusRing,\n        className,\n      )}\n      {...rest}\n    >\n      {icon}\n      {children}\n    </button>\n  );\n});\n\nexport type PillTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"error\";\n\nconst pillTones: Record<PillTone, string> = {\n  neutral: \"bg-[color:var(--bjork-hair)] text-[color:var(--bjork-text-medium)]\",\n  accent: \"bg-[color:var(--bjork-accent-soft)] text-[color:var(--bjork-accent-ink)]\",\n  success: \"bg-[color:var(--bjork-success-soft)] text-[color:var(--bjork-success)]\",\n  warning: \"bg-[color:var(--bjork-warning-soft)] text-[color:var(--bjork-warning)]\",\n  error: \"bg-[color:var(--bjork-error-soft)] text-[color:var(--bjork-error)]\",\n};\n\n/** Small status label. `dot` adds a leading status dot; `pulse` animates it (not under reduced motion). */\nexport function Pill({\n  tone = \"neutral\",\n  dot = false,\n  pulse = false,\n  className,\n  children,\n}: {\n  tone?: PillTone;\n  dot?: boolean;\n  pulse?: boolean;\n  className?: string;\n  children: ReactNode;\n}) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex h-[22px] shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2 text-[11px] font-medium leading-none\",\n        pillTones[tone],\n        className,\n      )}\n    >\n      {dot && (\n        <span aria-hidden=\"true\" className=\"relative inline-flex size-1.5\">\n          {pulse && (\n            <span className=\"absolute inset-0 rounded-full bg-current opacity-60 motion-safe:animate-ping\" />\n          )}\n          <span className=\"relative inline-flex size-1.5 rounded-full bg-current\" />\n        </span>\n      )}\n      {children}\n    </span>\n  );\n}\n\n/** Loading placeholder. Shimmers unless reduced motion is on. */\nexport function Skeleton({ className, style }: { className?: string; style?: CSSProperties }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\"block rounded-[6px] bg-[color:var(--bjork-hair)] motion-safe:animate-pulse\", className)}\n      style={style}\n    />\n  );\n}\n\n/** Deterministic pseudo-random generator for sample data, so server and client agree. */\nexport function seeded(seed: number) {\n  let s = seed >>> 0 || 1;\n  return () => {\n    s ^= s << 13;\n    s ^= s >>> 17;\n    s ^= s << 5;\n    return ((s >>> 0) % 100_000) / 100_000;\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/cards/card-kit.tsx"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/surface.ts",
      "content": "import type { CSSProperties } from \"react\";\nimport type { BjorkTone } from \"./palette\";\n\n// The library surface (CANON.md §1). Values mirror app/globals.css :root and .dark. Change both together.\nexport const BJORK_SURFACE = {\n  light: {\n    surface: \"#fffcf6\", surfaceMuted: \"rgba(250, 246, 237, 0.94)\", surfaceHover: \"#f5efe3\", surfaceActive: \"#efe7d8\",\n    panel: \"#f8f2e7\", menu: \"rgba(255, 252, 246, 0.98)\", field: \"rgba(255, 252, 246, 0.96)\",\n    fieldMuted: \"rgba(248, 242, 231, 0.82)\", fieldInset: \"rgba(239, 231, 216, 0.72)\",\n    border: \"#eee6db\", borderMuted: \"#f5ede2\", borderStrong: \"#e1d7c8\",\n    shadowSurface: \"inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)\",\n    shadowSoft: \"inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)\",\n    shadowPanel: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)\",\n    shadowMenu: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)\",\n    shadowInset: \"inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)\",\n  },\n  dark: {\n    surface: \"#121212\", surfaceMuted: \"rgba(22, 22, 22, 0.92)\", surfaceHover: \"#161616\", surfaceActive: \"#202020\",\n    panel: \"#0d0d0d\", menu: \"rgba(18, 18, 18, 0.98)\", field: \"rgba(18, 18, 18, 0.95)\",\n    fieldMuted: \"rgba(18, 18, 18, 0.55)\", fieldInset: \"rgba(9, 9, 9, 0.75)\",\n    border: \"#232323\", borderMuted: \"#1c1c1c\", borderStrong: \"#343434\",\n    shadowSurface: \"inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)\",\n    shadowSoft: \"inset 0 1px 0 rgba(255, 255, 255, 0.045)\",\n    shadowPanel: \"inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)\",\n    shadowMenu: \"inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)\",\n    shadowInset: \"inset 0 1px 10px rgba(0, 0, 0, 0.45)\",\n  },\n} as const;\n\n/** Writes the surface tokens for a pinned tone as --bjork-* custom properties. Spread first; kit-specific keys may override. */\nexport function surfaceVars(tone: BjorkTone): CSSProperties {\n  const s = BJORK_SURFACE[tone];\n  return {\n    \"--bjork-surface\": s.surface, \"--bjork-surface-muted\": s.surfaceMuted, \"--bjork-surface-hover\": s.surfaceHover,\n    \"--bjork-surface-active\": s.surfaceActive, \"--bjork-panel\": s.panel, \"--bjork-menu\": s.menu, \"--bjork-field\": s.field,\n    \"--bjork-field-muted\": s.fieldMuted, \"--bjork-field-inset\": s.fieldInset, \"--bjork-border\": s.border,\n    \"--bjork-border-muted\": s.borderMuted, \"--bjork-border-strong\": s.borderStrong,\n    \"--bjork-shadow-surface\": s.shadowSurface, \"--bjork-shadow-soft\": s.shadowSoft, \"--bjork-shadow-panel\": s.shadowPanel,\n    \"--bjork-shadow-menu\": s.shadowMenu, \"--bjork-shadow-inset\": s.shadowInset,\n  } as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/surface.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Cards",
    "registryUrl": "https://ui.isaiahbjork.com/payment-method-card.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/payment-method-card.json"
  },
  "type": "registry:component"
}