{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cost-meter",
  "title": "Cost meter",
  "description": "Cost meter is a production-ready ai component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/ai/cost-meter.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type KeyboardEvent, type PointerEvent } from \"react\";\nimport { motion, useSpring, useTransform } from \"framer-motion\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport { easeCss } from \"@/components/bjork-ui/_core/motion\";\nimport {\n  AI_PANEL,\n  FOCUS_RING,\n  formatTokens,\n  useAiTone,\n  useControllable,\n  useHydrated,\n  useReduceMotion,\n  type BjorkTone,\n} from \"@/components/bjork-ui/ai/_shared\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CostPeriod = \"today\" | \"7d\" | \"30d\";\n\nexport interface ModelUsage {\n  id: string;\n  model: string;\n  /** Uncached input tokens. */\n  inputTokens: number;\n  outputTokens: number;\n  /** Input tokens served from the prompt cache. */\n  cachedTokens: number;\n  /** Cost for the period, in `currency`. */\n  cost: number;\n  /** What the cached tokens would have cost at the full input price, minus what they did cost. */\n  cacheSavings?: number;\n}\n\nexport interface DailySpend {\n  /** ISO date, such as \"2026-10-08\". */\n  date: string;\n  cost: number;\n}\n\nexport interface CostMeterProps {\n  /** Spend for the selected period. */\n  spend: number;\n  /** Budget for the selected period. Amber at 80%, red at 100%, overage hatched. */\n  budget: number;\n  breakdown?: ModelUsage[];\n  /** Daily spend, oldest first. The last 14 entries are drawn. */\n  history?: DailySpend[];\n  period?: CostPeriod;\n  defaultPeriod?: CostPeriod;\n  onPeriodChange?: (period: CostPeriod) => void;\n  /** ISO 4217 code. Default \"USD\". */\n  currency?: string;\n  /** Header label. Default \"Spend\". */\n  label?: string;\n  /** Demo/preview only: shows the crosshair on this history point (0-based, of the drawn points) on mount. */\n  defaultActivePoint?: number;\n  tone?: BjorkTone;\n  className?: string;\n}\n\ninterface PeriodUsage {\n  spend: number;\n  budget: number;\n  breakdown: ModelUsage[];\n}\n\nconst HISTORY_VALUES = [0.31, 0.42, 0.28, 0.55, 0.61, 0.38, 0.22, 0.44, 0.63, 0.68, 0.5, 0.55, 0.66, 0.84];\n\nexport const SAMPLE_USAGE: { history: DailySpend[]; periods: Record<CostPeriod, PeriodUsage> } = {\n  // Fourteen days ending Oct 8, 2026.\n  history: HISTORY_VALUES.map((cost, i) => ({ date: `2026-${i < 6 ? \"09\" : \"10\"}-${String(i < 6 ? 25 + i : i - 5).padStart(2, \"0\")}`, cost })),\n  periods: {\n    today: {\n      spend: 0.84,\n      budget: 0.75,\n      breakdown: [\n        { id: \"halcyon-3-pro\", model: \"Halcyon 3 Pro\", inputTokens: 148_000, outputTokens: 17_600, cachedTokens: 96_000, cost: 0.64, cacheSavings: 0.26 },\n        { id: \"corvid-reasoner\", model: \"Corvid Reasoner\", inputTokens: 26_000, outputTokens: 4_100, cachedTokens: 0, cost: 0.17 },\n        { id: \"kestrel-mini\", model: \"Kestrel Mini\", inputTokens: 92_000, outputTokens: 18_400, cachedTokens: 31_000, cost: 0.03 },\n      ],\n    },\n    \"7d\": {\n      spend: 4.3,\n      budget: 5,\n      breakdown: [\n        { id: \"halcyon-3-pro\", model: \"Halcyon 3 Pro\", inputTokens: 812_000, outputTokens: 94_000, cachedTokens: 560_000, cost: 3.41, cacheSavings: 1.51 },\n        { id: \"corvid-reasoner\", model: \"Corvid Reasoner\", inputTokens: 128_000, outputTokens: 29_000, cachedTokens: 0, cost: 0.75 },\n        { id: \"kestrel-mini\", model: \"Kestrel Mini\", inputTokens: 384_000, outputTokens: 71_000, cachedTokens: 120_000, cost: 0.14, cacheSavings: 0.02 },\n      ],\n    },\n    \"30d\": {\n      spend: 14.4,\n      budget: 20,\n      breakdown: [\n        { id: \"halcyon-3-pro\", model: \"Halcyon 3 Pro\", inputTokens: 2_840_000, outputTokens: 312_000, cachedTokens: 1_900_000, cost: 11.62, cacheSavings: 5.13 },\n        { id: \"corvid-reasoner\", model: \"Corvid Reasoner\", inputTokens: 410_000, outputTokens: 96_000, cachedTokens: 0, cost: 2.31 },\n        { id: \"kestrel-mini\", model: \"Kestrel Mini\", inputTokens: 1_200_000, outputTokens: 240_000, cachedTokens: 400_000, cost: 0.47, cacheSavings: 0.05 },\n      ],\n    },\n  },\n};\n\nconst PERIODS: { id: CostPeriod; label: string; long: string }[] = [\n  { id: \"today\", label: \"Today\", long: \"today\" },\n  { id: \"7d\", label: \"7d\", long: \"the last 7 days\" },\n  { id: \"30d\", label: \"30d\", long: \"the last 30 days\" },\n];\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst HATCH =\n  \"repeating-linear-gradient(-45deg, var(--bjork-error) 0 2px, color-mix(in srgb, var(--bjork-error) 28%, transparent) 2px 4.5px)\";\n\nfunction shortDate(iso: string): string {\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(iso);\n  return m ? `${MONTHS[Number(m[2]) - 1]} ${Number(m[3])}` : iso;\n}\n\nfunction money(n: number, currency: string): string {\n  return new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);\n}\n\n/**\n * Spend against a budget: a large springing figure, a budget bar that turns amber then red, a per-model breakdown\n * with cache savings, and a 14-day sparkline you can scrub with a pointer or the arrow keys.\n */\nexport function CostMeter({\n  spend,\n  budget,\n  breakdown = [],\n  history = [],\n  period,\n  defaultPeriod = \"30d\",\n  onPeriodChange,\n  currency = \"USD\",\n  label = \"Spend\",\n  defaultActivePoint,\n  tone: toneProp,\n  className,\n}: CostMeterProps) {\n  const { style } = useAiTone(toneProp);\n  const reduce = useReduceMotion();\n  const hydrated = useHydrated();\n  const [current, setPeriod] = useControllable(period, defaultPeriod, onPeriodChange);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const baseId = useId();\n  const labelId = `${baseId}-label`;\n\n  const ratio = budget > 0 ? spend / budget : 0;\n  const level = ratio >= 1 ? \"over\" : ratio >= 0.8 ? \"warn\" : \"ok\";\n  const levelColor = level === \"over\" ? \"var(--bjork-error)\" : level === \"warn\" ? \"var(--bjork-warning)\" : \"var(--bjork-accent)\";\n  const base = Math.max(budget, spend) || 1;\n  const within = Math.min(spend, budget) / base;\n  const over = Math.max(0, spend - budget) / base;\n  const fmt = (n: number) => money(n, currency);\n\n  // The figure springs to new values; reduced motion jumps.\n  const figure = useSpring(spend, { stiffness: 140, damping: 24, mass: 0.9 });\n  const figureText = useTransform(figure, (v) => fmt(Math.max(0, v)));\n  useEffect(() => {\n    if (reduce) figure.jump(spend);\n    else figure.set(spend);\n  }, [spend, reduce, figure]);\n\n  // Announce crossing into amber or red.\n  const [seen, setSeen] = useState({ level, message: \"\" });\n  if (seen.level !== level) {\n    setSeen({\n      level,\n      message:\n        level === \"over\" ? `Over budget: ${fmt(spend)} of ${fmt(budget)}` : level === \"warn\" ? `${Math.round(ratio * 100)}% of budget used` : \"\",\n    });\n  }\n\n  const onTabKey = (e: KeyboardEvent<HTMLButtonElement>, i: number) => {\n    const delta = e.key === \"ArrowRight\" ? 1 : e.key === \"ArrowLeft\" ? -1 : 0;\n    let next = delta ? (i + delta + PERIODS.length) % PERIODS.length : -1;\n    if (e.key === \"Home\") next = 0;\n    if (e.key === \"End\") next = PERIODS.length - 1;\n    if (next < 0) return;\n    e.preventDefault();\n    setPeriod(PERIODS[next].id);\n    tabRefs.current[next]?.focus();\n  };\n\n  const transition = reduce || !hydrated ? \"none\" : `transform 520ms ${easeCss.out}, background-color 200ms ease-out`;\n\n  return (\n    <section\n      aria-labelledby={labelId}\n      className={cn(AI_PANEL, \"@container w-full max-w-[560px] font-bjork-alpha text-[color:var(--bjork-text)]\", className)}\n      style={style}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <h2 id={labelId} className=\"text-[14px] font-semibold leading-5\">\n          {label}\n        </h2>\n        <div role=\"tablist\" aria-label=\"Period\" className=\"flex rounded-[9px] bg-[color:var(--bjork-surface-active)] p-0.5\">\n          {PERIODS.map((p, i) => {\n            const selected = p.id === current;\n            return (\n              <button\n                key={p.id}\n                ref={(el) => {\n                  tabRefs.current[i] = el;\n                }}\n                type=\"button\"\n                role=\"tab\"\n                id={`${baseId}-tab-${p.id}`}\n                aria-selected={selected}\n                aria-controls={`${baseId}-panel`}\n                tabIndex={selected ? 0 : -1}\n                onClick={() => setPeriod(p.id)}\n                onKeyDown={(e) => onTabKey(e, i)}\n                className={cn(\n                  \"h-7 min-w-[44px] cursor-pointer rounded-[7px] px-2.5 font-mono text-[11px] transition-[background-color,color,box-shadow] duration-150\",\n                  selected\n                    ? \"bg-[color:var(--bjork-field)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)]\"\n                    : \"text-[color:var(--bjork-text-muted)] hover:text-[color:var(--bjork-text)]\",\n                  FOCUS_RING,\n                )}\n              >\n                {p.label}\n              </button>\n            );\n          })}\n        </div>\n      </div>\n\n      <div id={`${baseId}-panel`} role=\"tabpanel\" aria-labelledby={`${baseId}-tab-${current}`}>\n        <div className=\"mt-4 flex flex-wrap items-baseline gap-x-2.5 gap-y-1\">\n          <motion.span className=\"min-w-[5ch] text-[40px] font-semibold leading-[44px] tracking-[-0.03em] tabular-nums\">\n            {figureText}\n          </motion.span>\n          <span className=\"font-mono text-[13px] tabular-nums text-[color:var(--bjork-text-muted)]\">of {fmt(budget)}</span>\n          <span\n            className=\"ml-auto min-w-[4ch] text-right font-mono text-[12px] tabular-nums transition-colors duration-200\"\n            style={{ color: level === \"ok\" ? \"var(--bjork-text-muted)\" : levelColor }}\n          >\n            {level === \"over\" ? `${fmt(spend - budget)} over` : `${Math.round(ratio * 100)}%`}\n          </span>\n        </div>\n\n        <div\n          role=\"meter\"\n          aria-labelledby={labelId}\n          aria-valuemin={0}\n          aria-valuemax={budget}\n          aria-valuenow={Math.min(spend, budget)}\n          aria-valuetext={`${fmt(spend)} of ${fmt(budget)} budget for ${PERIODS.find((p) => p.id === current)?.long}, ${Math.round(ratio * 100)}%`}\n          className=\"relative mt-3\"\n        >\n          <div className=\"relative h-2 overflow-hidden rounded-full bg-[color:var(--bjork-hair)]\">\n            <div\n              className=\"absolute inset-0 origin-left rounded-full\"\n              style={{ transform: `scaleX(${hydrated ? within : 0})`, background: levelColor, transition }}\n            />\n            <div\n              aria-hidden=\"true\"\n              className=\"absolute inset-0 origin-left\"\n              style={{\n                transform: `translateX(${within * 100}%) scaleX(${hydrated ? over : 0})`,\n                background: HATCH,\n                transition,\n              }}\n            />\n          </div>\n          {/* Budget line when over, and the 80% warning tick. */}\n          <div aria-hidden=\"true\" className=\"relative h-2\">\n            <span\n              className=\"absolute top-1 h-1.5 w-px -translate-x-1/2 bg-[color:var(--bjork-warning)]\"\n              style={{ left: `${(budget * 0.8 * 100) / base}%`, opacity: level === \"ok\" ? 0.45 : 0.9 }}\n            />\n            {level === \"over\" && (\n              <span\n                className=\"absolute -top-3 h-[18px] w-0.5 -translate-x-1/2 rounded-full bg-[color:var(--bjork-text)]\"\n                style={{ left: `${(budget * 100) / base}%` }}\n              />\n            )}\n          </div>\n        </div>\n\n        {breakdown.length > 0 && <Breakdown rows={breakdown} fmt={fmt} />}\n      </div>\n\n      {history.length > 1 && <Sparkline points={history.slice(-14)} fmt={fmt} defaultActive={defaultActivePoint} />}\n\n      <LiveRegion message={seen.message} />\n    </section>\n  );\n}\n\nfunction Breakdown({ rows, fmt }: { rows: ModelUsage[]; fmt: (n: number) => string }) {\n  const th = \"pb-1.5 text-right font-mono text-[10px] font-normal uppercase tracking-[0.08em] text-[color:var(--bjork-text-faint)]\";\n  return (\n    <table className=\"mt-4 w-full table-fixed border-collapse\">\n      <caption className=\"sr-only\">Usage by model</caption>\n      <colgroup>\n        <col />\n        <col className=\"w-[30%] @[440px]:w-[26%]\" />\n        <col className=\"w-[22%] @[440px]:w-[20%]\" />\n        <col className=\"w-[18%] @[440px]:w-[16%]\" />\n      </colgroup>\n      <thead>\n        <tr className=\"border-b border-[color:var(--bjork-border)]\">\n          <th scope=\"col\" className={cn(th, \"text-left\")}>\n            Model\n          </th>\n          <th scope=\"col\" className={th}>\n            In / out\n          </th>\n          <th scope=\"col\" className={th}>\n            Cached\n          </th>\n          <th scope=\"col\" className={th}>\n            Cost\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {rows.map((r) => (\n          <tr key={r.id} className=\"border-b border-[color:var(--bjork-border)] last:border-b-0\">\n            <th scope=\"row\" className=\"truncate py-2 pr-2 text-left text-[13px] font-medium leading-5\">\n              {r.model}\n            </th>\n            <td className=\"py-2 text-right font-mono text-[11.5px] leading-5 tabular-nums text-[color:var(--bjork-text-muted)]\">\n              {formatTokens(r.inputTokens)}\n              <span className=\"text-[color:var(--bjork-text-faint)]\"> / </span>\n              {formatTokens(r.outputTokens)}\n            </td>\n            <td className=\"py-2 text-right align-top font-mono text-[11.5px] leading-5 tabular-nums\">\n              <span className={r.cachedTokens ? \"text-[color:var(--bjork-text-muted)]\" : \"text-[color:var(--bjork-text-faint)]\"}>\n                {r.cachedTokens ? formatTokens(r.cachedTokens) : \"—\"}\n              </span>\n              {r.cacheSavings ? (\n                <span className=\"block text-[11px] leading-4 text-[color:var(--bjork-success)]\">−{fmt(r.cacheSavings)}</span>\n              ) : null}\n            </td>\n            <td className=\"py-2 text-right align-top font-mono text-[12px] leading-5 tabular-nums text-[color:var(--bjork-text)]\">\n              {fmt(r.cost)}\n            </td>\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  );\n}\n\nconst CHART_H = 64;\nconst PAD_TOP = 8;\n\nfunction Sparkline({ points, fmt, defaultActive }: { points: DailySpend[]; fmt: (n: number) => string; defaultActive?: number }) {\n  const [active, setActive] = useState<number | null>(defaultActive ?? null);\n  const [said, setSaid] = useState(\"\");\n  const boxRef = useRef<HTMLDivElement>(null);\n  const summaryId = useId();\n  // useId output contains characters that break url(#id) references.\n  const gradId = `spend-grad-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n  const n = points.length;\n  const max = Math.max(...points.map((p) => p.cost), 0.0001);\n  const total = points.reduce((s, p) => s + p.cost, 0);\n  const peak = points.reduce((best, p, i) => (p.cost > points[best].cost ? i : best), 0);\n  // x in 0..100 (percent of width), y in px.\n  const xs = points.map((_, i) => (i / (n - 1)) * 100);\n  const ys = points.map((p) => PAD_TOP + (1 - p.cost / max) * (CHART_H - PAD_TOP - 2));\n  const line = points.map((_, i) => `${i ? \"L\" : \"M\"}${xs[i]},${ys[i]}`).join(\" \");\n  const area = `${line} L100,${CHART_H} L0,${CHART_H} Z`;\n\n  const point = active !== null ? points[active] : null;\n  const say = (i: number) => setSaid(`${shortDate(points[i].date)}: ${fmt(points[i].cost)}`);\n\n  const fromPointer = (e: PointerEvent<HTMLDivElement>) => {\n    const r = boxRef.current?.getBoundingClientRect();\n    if (!r) return;\n    const i = Math.round(((e.clientX - r.left) / r.width) * (n - 1));\n    setActive(Math.min(n - 1, Math.max(0, i)));\n  };\n\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    const cur = active ?? n - 1;\n    let next = cur;\n    if (e.key === \"ArrowRight\") next = Math.min(n - 1, cur + 1);\n    else if (e.key === \"ArrowLeft\") next = Math.max(0, cur - 1);\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = n - 1;\n    else if (e.key === \"Escape\") {\n      setActive(null);\n      return;\n    } else return;\n    e.preventDefault();\n    setActive(next);\n    say(next);\n  };\n\n  // Tooltip hugs the crosshair but never leaves the chart: pinned left near the start, right near the end.\n  const tipAlign = active === null ? 0 : active / (n - 1);\n\n  return (\n    <div className=\"mt-5\">\n      <div className=\"mb-2 flex items-baseline justify-between gap-3\">\n        <span className=\"font-mono text-[10px] uppercase tracking-[0.08em] text-[color:var(--bjork-text-faint)]\">Last {n} days</span>\n        <span className=\"font-mono text-[11px] tabular-nums text-[color:var(--bjork-text-muted)]\">{fmt(total)}</span>\n      </div>\n      <p id={summaryId} className=\"sr-only\">\n        Daily spend over the last {n} days, {fmt(total)} in total, highest on {shortDate(points[peak].date)} at {fmt(points[peak].cost)}.\n        Use the left and right arrow keys to read each day.\n      </p>\n      <div\n        ref={boxRef}\n        tabIndex={0}\n        role=\"group\"\n        aria-label=\"Daily spend chart\"\n        aria-describedby={summaryId}\n        onKeyDown={onKey}\n        onFocus={() => {\n          if (active === null) {\n            setActive(n - 1);\n            say(n - 1);\n          }\n        }}\n        onBlur={() => setActive(null)}\n        onPointerMove={fromPointer}\n        onPointerDown={fromPointer}\n        onPointerLeave={(e) => {\n          if (e.pointerType === \"mouse\" && document.activeElement !== boxRef.current) setActive(null);\n        }}\n        className={cn(\"relative cursor-crosshair touch-pan-y rounded-[6px]\", FOCUS_RING)}\n        style={{ height: CHART_H }}\n      >\n        <svg width=\"100%\" height={CHART_H} viewBox={`0 0 100 ${CHART_H}`} preserveAspectRatio=\"none\" aria-hidden=\"true\" className=\"block overflow-visible\">\n          <defs>\n            <linearGradient id={gradId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0%\" stopColor=\"var(--bjork-accent)\" stopOpacity=\"0.16\" />\n              <stop offset=\"100%\" stopColor=\"var(--bjork-accent)\" stopOpacity=\"0\" />\n            </linearGradient>\n          </defs>\n          <line x1=\"0\" x2=\"100\" y1={CHART_H - 0.5} y2={CHART_H - 0.5} stroke=\"var(--bjork-border)\" strokeWidth=\"1\" vectorEffect=\"non-scaling-stroke\" />\n          <path d={area} fill={`url(#${gradId})`} />\n          <path\n            d={line}\n            fill=\"none\"\n            stroke=\"var(--bjork-accent)\"\n            strokeWidth=\"1.75\"\n            strokeLinejoin=\"round\"\n            strokeLinecap=\"round\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        </svg>\n        {/* The crosshair and dot are HTML so they stay round and crisp under the stretched viewBox. */}\n        {point && active !== null && (\n          <>\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute bottom-0 top-0 w-px -translate-x-1/2 bg-[color:var(--bjork-text-faint)]\"\n              style={{ left: `${xs[active]}%` }}\n            />\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute size-[9px] -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-[color:var(--bjork-accent)] bg-[color:var(--bjork-ring-offset)]\"\n              style={{ left: `${xs[active]}%`, top: ys[active] }}\n            />\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute bottom-[calc(100%+6px)] whitespace-nowrap rounded-[7px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-menu)] px-2 py-1 font-mono text-[11px] leading-4 tabular-nums shadow-[var(--bjork-shadow-menu)]\"\n              style={{ left: `${xs[active]}%`, transform: `translateX(-${tipAlign * 100}%)` }}\n            >\n              <span className=\"text-[color:var(--bjork-text-muted)]\">{shortDate(point.date)}</span>{\" \"}\n              <span className=\"text-[color:var(--bjork-text)]\">{fmt(point.cost)}</span>\n            </span>\n          </>\n        )}\n      </div>\n      <div aria-hidden=\"true\" className=\"mt-1.5 flex justify-between font-mono text-[10px] tabular-nums text-[color:var(--bjork-text-faint)]\">\n        <span>{shortDate(points[0].date)}</span>\n        <span>{shortDate(points[n - 1].date)}</span>\n      </div>\n      <LiveRegion message={said} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/cost-meter.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/ai/_shared.ts",
      "content": "\"use client\";\n\n// Shared plumbing for the bjork-ui AI components: tone tokens, reduced motion, controllable state and\n// number formatting. Every AI component imports from here so they read as one family.\n\nimport {\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { surfaceVars } from \"@/components/bjork-ui/_core/surface\";\n\nexport type { BjorkTone };\nexport type BjorkPalette = (typeof BJORK_PALETTE)[BjorkTone];\n\nconst noopSubscribe = () => () => {};\n\n/** False on the server and the first client render, true after hydration. */\nexport function useHydrated(): boolean {\n  return useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false,\n  );\n}\n\n/** Hydration-safe reduced motion: both sides report \"not reduced\" until mount, then the real value applies. */\nexport function useReduceMotion(): boolean {\n  const hydrated = useHydrated();\n  const reduced = useReducedMotion();\n  return hydrated && reduced === true;\n}\n\n/**\n * Resolves the tone and returns the CSS custom properties for it. Spread `style` on the component root so an\n * explicit `tone` prop wins over the page theme, and status colours (success, warning, error) are always defined.\n */\nexport function useAiTone(toneProp?: BjorkTone) {\n  const tone = useBjorkTone(toneProp);\n  const pal = BJORK_PALETTE[tone];\n  const style = {\n    ...surfaceVars(tone),\n    \"--bjork-text\": pal.text,\n    \"--bjork-text-medium\": pal.textMedium,\n    \"--bjork-text-muted\": pal.textMuted,\n    \"--bjork-text-soft\": pal.textSoft,\n    \"--bjork-text-faint\": pal.textFaint,\n    \"--bjork-hair\": pal.hair,\n    \"--bjork-border\": pal.border,\n    \"--bjork-border-strong\": pal.borderStrong,\n    \"--bjork-surface\": pal.surface,\n    \"--bjork-raised\": pal.raised,\n    \"--bjork-surface-active\": pal.active,\n    \"--bjork-accent\": pal.accent,\n    \"--bjork-accent-ink\": pal.accentInk,\n    \"--bjork-accent-fill\": pal.accentFill,\n    \"--bjork-accent-soft\": pal.accentSoft,\n    \"--bjork-accent-muted\": pal.accentMuted,\n    \"--bjork-accent-foreground\": pal.accentFg,\n    \"--bjork-success\": pal.success,\n    \"--bjork-warning\": pal.warning,\n    \"--bjork-error\": pal.error,\n    \"--bjork-ring-offset\": pal.bg,\n    colorScheme: tone,\n  } as CSSProperties;\n  return { tone, pal, style };\n}\n\n/** Classes for the shared keyboard focus ring. */\nexport const FOCUS_RING =\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\n/** Classes for the shared press feedback. */\nexport const PRESS = \"transition-transform duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none\";\n\n/**\n * Controlled-or-uncontrolled state. Pass `value` to control it; `onChange` fires for every change either way.\n */\nexport function useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange?: (next: T) => void,\n): [T, (next: T) => void] {\n  const controlled = value !== undefined;\n  const [inner, setInner] = useState(defaultValue);\n  const current = controlled ? value : inner;\n  const onChangeRef = useRef(onChange);\n  useLayoutEffect(() => {\n    onChangeRef.current = onChange;\n  });\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInner(next);\n      onChangeRef.current?.(next);\n    },\n    [controlled],\n  );\n  return [current, set];\n}\n\n/** 1234 -> \"1,234\", 18400 -> \"18.4k\", 1_200_000 -> \"1.2M\". */\nexport function formatTokens(n: number): string {\n  if (n < 10_000) return Math.round(n).toLocaleString(\"en-US\");\n  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0).replace(/\\.0$/, \"\")}k`;\n  return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 2 : 1).replace(/\\.?0+$/, \"\")}M`;\n}\n\n/** US dollars with sub-cent precision when it matters: 0.0042 -> \"$0.0042\", 12.5 -> \"$12.50\". */\nexport function formatCost(usd: number): string {\n  if (usd === 0) return \"$0.00\";\n  if (Math.abs(usd) < 0.01) return `$${usd.toFixed(4)}`;\n  if (Math.abs(usd) < 1) return `$${usd.toFixed(3)}`;\n  return `$${usd.toLocaleString(\"en-US\", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;\n}\n\n/** 640 -> \"640ms\", 4200 -> \"4.2s\", 95000 -> \"1m 35s\". */\nexport function formatDuration(ms: number): string {\n  if (ms < 1000) return `${Math.round(ms)}ms`;\n  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n  const m = Math.floor(ms / 60_000);\n  const s = Math.round((ms % 60_000) / 1000);\n  return `${m}m ${s}s`;\n}\n\n/** 2_400_000 bytes -> \"2.4 MB\". */\nexport function formatBytes(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\"];\n  let v = bytes / 1024;\n  let i = 0;\n  while (v >= 1024 && i < units.length - 1) {\n    v /= 1024;\n    i++;\n  }\n  return `${v.toFixed(v < 10 ? 1 : 0)} ${units[i]}`;\n}\n\n/** Shimmer keyframes for \"working\" text. Mount once with <style href=... precedence=\"default\">. */\nexport const SHIMMER_TEXT_CSS =\n  \"@keyframes bjork-ai-shimmer{from{background-position:100% 0}to{background-position:0% 0}}\" +\n  \".bjork-ai-shimmer{background-image:linear-gradient(90deg,var(--bjork-text-muted) 0%,var(--bjork-text-muted) 40%,var(--bjork-text) 50%,var(--bjork-text-muted) 60%,var(--bjork-text-muted) 100%);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:bjork-ai-shimmer 1.8s linear infinite}\" +\n  \"@media (prefers-reduced-motion: reduce){.bjork-ai-shimmer{animation:none;background:none;color:var(--bjork-text-muted)}}\";\n\n/** CANON §2. Panel = a component's own root surface; Card = a standalone card; Tile = a card inside a panel; Well = recessed code/field. */\nexport const AI_PANEL = \"rounded-[20px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] p-4 shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_CARD = \"rounded-[18px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_TILE = \"rounded-[12px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_WELL = \"rounded-[8px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-field-inset)] shadow-[var(--bjork-shadow-inset)]\";\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/_shared.ts"
    },
    {
      "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/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": "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": "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": "AI",
    "registryUrl": "https://ui.isaiahbjork.com/cost-meter.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/cost-meter.json"
  },
  "type": "registry:component"
}