{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-dashboard-shell",
  "title": "Dashboard shell",
  "description": "Dashboard shell is a production-ready blocks component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/blocks/app-dashboard-shell.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentType,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  type SVGProps,\n} from \"react\";\nimport {\n  ArrowDown,\n  ArrowDownRight,\n  ArrowUp,\n  ArrowUpDown,\n  ArrowUpRight,\n  Bell,\n  Blocks,\n  ChartArea,\n  ChartNoAxesCombined,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsUpDown,\n  Download,\n  FileText,\n  LayoutDashboard,\n  LifeBuoy,\n  Menu,\n  Package,\n  PanelLeftClose,\n  PanelLeftOpen,\n  Search,\n  Settings,\n  ShoppingBag,\n  Table2,\n  Users,\n  UsersRound,\n  X,\n} from \"lucide-react\";\nimport {\n  BlockButton,\n  BlockDrawer,\n  IconButton,\n  InitialsAvatar,\n  Kbd,\n  blockRoot,\n  focusRing,\n  useAnnouncer,\n  useAppBlockTheme,\n  useBlockReducedMotion,\n  useElementWidth,\n  type AppBlockTheme,\n} from \"@/components/bjork-ui/blocks/app-block-kit\";\nimport { cn } from \"@/lib/utils\";\n\n/* ----------------------------------------------------------------------------------------------------------\n * Types\n * -------------------------------------------------------------------------------------------------------- */\n\nexport type DashboardRange = \"7d\" | \"30d\" | \"90d\" | \"12m\";\n\nexport type OrderStatus = \"paid\" | \"pending\" | \"refunded\" | \"failed\";\n\nexport interface DashboardOrder {\n  id: string;\n  customer: string;\n  email: string;\n  status: OrderStatus;\n  channel: string;\n  /** ISO date, YYYY-MM-DD. */\n  date: string;\n  /** Minor units (cents). */\n  amount: number;\n}\n\nexport interface DashboardNavItem {\n  id: string;\n  label: string;\n  icon: ComponentType<SVGProps<SVGSVGElement>>;\n  badge?: string;\n}\n\nexport interface DashboardNavGroup {\n  label?: string;\n  items: DashboardNavItem[];\n}\n\nexport interface DashboardPoint {\n  label: string;\n  /** Short axis label. */\n  tick: string;\n  current: number;\n  previous: number;\n}\n\nexport interface DashboardKpi {\n  id: string;\n  label: string;\n  value: number;\n  previous: number;\n  format: \"currency\" | \"number\" | \"percent\";\n  /** For churn-like metrics a fall is good news. */\n  lowerIsBetter?: boolean;\n  spark: number[];\n}\n\nexport interface DashboardSeries {\n  points: DashboardPoint[];\n  kpis: DashboardKpi[];\n  channels: { label: string; value: number }[];\n}\n\nexport interface AppDashboardShellProps {\n  /** Workspace shown in the switcher. */\n  workspace?: { name: string; plan: string };\n  user?: { name: string; email: string };\n  nav?: DashboardNavGroup[];\n  /** Id of the active nav item. Uncontrolled if `onNavigate` is not handled. */\n  defaultActive?: string;\n  onNavigate?: (id: string) => void;\n  orders?: DashboardOrder[];\n  /** Data per range. Defaults to deterministic sample data. */\n  getSeries?: (range: DashboardRange) => DashboardSeries;\n  defaultRange?: DashboardRange;\n  onRangeChange?: (range: DashboardRange) => void;\n  defaultCollapsed?: boolean;\n  currency?: string;\n  locale?: string;\n  theme?: AppBlockTheme;\n  /** Skips the chart reveal; for static captures. */\n  disableAnimation?: boolean;\n  className?: string;\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Sample data\n * -------------------------------------------------------------------------------------------------------- */\n\nexport const DASHBOARD_NAV: DashboardNavGroup[] = [\n  {\n    items: [\n      { id: \"overview\", label: \"Overview\", icon: LayoutDashboard },\n      { id: \"analytics\", label: \"Analytics\", icon: ChartNoAxesCombined },\n      { id: \"orders\", label: \"Orders\", icon: ShoppingBag, badge: \"12\" },\n      { id: \"customers\", label: \"Customers\", icon: Users },\n      { id: \"products\", label: \"Products\", icon: Package },\n      { id: \"reports\", label: \"Reports\", icon: FileText },\n    ],\n  },\n  {\n    label: \"Workspace\",\n    items: [\n      { id: \"team\", label: \"Team\", icon: UsersRound },\n      { id: \"integrations\", label: \"Integrations\", icon: Blocks, badge: \"New\" },\n      { id: \"settings\", label: \"Settings\", icon: Settings },\n    ],\n  },\n];\n\nexport const DASHBOARD_ORDERS: DashboardOrder[] = [\n  { id: \"HL-48213\", customer: \"Maren Okafor\", email: \"maren@fieldnote.studio\", status: \"paid\", channel: \"Direct\", date: \"2026-10-08\", amount: 248_00 },\n  { id: \"HL-48212\", customer: \"Tomás Reyes\", email: \"tomas@kilnworks.co\", status: \"pending\", channel: \"Partner\", date: \"2026-10-08\", amount: 1_120_00 },\n  { id: \"HL-48211\", customer: \"Ines Halvorsen\", email: \"ines@northpier.no\", status: \"paid\", channel: \"Organic\", date: \"2026-10-07\", amount: 89_00 },\n  { id: \"HL-48210\", customer: \"Dev Raman\", email: \"dev@quietloop.io\", status: \"refunded\", channel: \"Paid social\", date: \"2026-10-07\", amount: 312_50 },\n  { id: \"HL-48209\", customer: \"Aiko Brandt\", email: \"aiko@lumenhaus.de\", status: \"paid\", channel: \"Direct\", date: \"2026-10-06\", amount: 2_430_00 },\n  { id: \"HL-48208\", customer: \"Callum Ferris\", email: \"callum@saltmarsh.co.uk\", status: \"failed\", channel: \"Email\", date: \"2026-10-06\", amount: 64_00 },\n  { id: \"HL-48207\", customer: \"Noor Al-Sayed\", email: \"noor@cedarline.ae\", status: \"paid\", channel: \"Organic\", date: \"2026-10-05\", amount: 575_00 },\n  { id: \"HL-48206\", customer: \"Pia Lindqvist\", email: \"pia@arkivet.se\", status: \"paid\", channel: \"Partner\", date: \"2026-10-05\", amount: 1_860_00 },\n  { id: \"HL-48205\", customer: \"Jonah Whitfield\", email: \"jonah@brightwater.ca\", status: \"pending\", channel: \"Direct\", date: \"2026-10-04\", amount: 420_00 },\n  { id: \"HL-48204\", customer: \"Sofia Marchetti\", email: \"sofia@vetro.it\", status: \"paid\", channel: \"Email\", date: \"2026-10-04\", amount: 156_00 },\n  { id: \"HL-48203\", customer: \"Kwame Asante\", email: \"kwame@goldcoast.dev\", status: \"paid\", channel: \"Paid social\", date: \"2026-10-03\", amount: 990_00 },\n  { id: \"HL-48202\", customer: \"Elodie Garnier\", email: \"elodie@atelier-gris.fr\", status: \"refunded\", channel: \"Direct\", date: \"2026-10-03\", amount: 210_00 },\n  { id: \"HL-48201\", customer: \"Hugo Ferreira\", email: \"hugo@marfim.pt\", status: \"paid\", channel: \"Organic\", date: \"2026-10-02\", amount: 3_150_00 },\n  { id: \"HL-48200\", customer: \"Yara Lindgren\", email: \"yara@polarlab.fi\", status: \"paid\", channel: \"Partner\", date: \"2026-10-02\", amount: 740_00 },\n];\n\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\n// Seeded so server and client render the same sample.\nfunction mulberry32(seed: number) {\n  let a = seed;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst RANGE_SHAPE: Record<DashboardRange, { count: number; seed: number; base: number; unit: \"day\" | \"week\" | \"month\" }> = {\n  \"7d\": { count: 7, seed: 7, base: 18_400, unit: \"day\" },\n  \"30d\": { count: 30, seed: 30, base: 17_600, unit: \"day\" },\n  \"90d\": { count: 13, seed: 90, base: 121_000, unit: \"week\" },\n  \"12m\": { count: 12, seed: 12, base: 486_000, unit: \"month\" },\n};\n\nconst ANCHOR = Date.UTC(2026, 9, 8); // 8 Oct 2026, the sample's \"today\"\nconst DAY = 86_400_000;\n\nfunction utcLabel(ms: number) {\n  const d = new Date(ms);\n  return { day: d.getUTCDate(), month: MONTHS[d.getUTCMonth()], year: d.getUTCFullYear() };\n}\n\n/** Deterministic sample series for a range. */\nexport function sampleDashboardSeries(range: DashboardRange): DashboardSeries {\n  const { count, seed, base, unit } = RANGE_SHAPE[range];\n  const rand = mulberry32(seed);\n  const points: DashboardPoint[] = [];\n  let level = base * 0.86;\n  let prevLevel = base * 0.8;\n  for (let i = 0; i < count; i += 1) {\n    const progress = i / Math.max(1, count - 1);\n    const weekly = unit === \"day\" ? Math.sin(((i + 2) / 7) * Math.PI * 2) * 0.07 : 0;\n    level = level * (1 + (rand() - 0.42) * 0.09) + base * 0.012;\n    prevLevel = prevLevel * (1 + (rand() - 0.47) * 0.08) + base * 0.006;\n    const current = Math.round(level * (1 + weekly) * (0.94 + progress * 0.12));\n    const previous = Math.round(prevLevel * (1 + weekly * 0.8));\n    let label: string;\n    let tick: string;\n    if (unit === \"month\") {\n      const m = (9 - (count - 1 - i) + 24) % 12;\n      const y = 2026 - (9 - (count - 1 - i) < 0 ? 1 : 0);\n      label = `${MONTHS[m]} ${y}`;\n      tick = MONTHS[m];\n    } else {\n      const step = unit === \"week\" ? 7 : 1;\n      const ms = ANCHOR - (count - 1 - i) * step * DAY;\n      const d = utcLabel(ms);\n      label = unit === \"week\" ? `Week of ${d.month} ${d.day}` : `${d.month} ${d.day}, ${d.year}`;\n      tick = `${d.month} ${d.day}`;\n    }\n    points.push({ label, tick, current, previous });\n  }\n\n  const revenue = points.reduce((s, p) => s + p.current, 0);\n  const revenuePrev = points.reduce((s, p) => s + p.previous, 0);\n  const scale = revenue / 520_000;\n  const spark = (k: number, drift: number) => {\n    const r = mulberry32(seed * 31 + k);\n    let v = 50;\n    return Array.from({ length: 14 }, () => (v = Math.max(8, v + (r() - 0.5 + drift) * 14)));\n  };\n\n  return {\n    points,\n    kpis: [\n      { id: \"revenue\", label: \"Net revenue\", value: revenue, previous: revenuePrev, format: \"currency\", spark: points.slice(-14).map((p) => p.current) },\n      { id: \"customers\", label: \"Active customers\", value: Math.round(2_380 * Math.sqrt(scale)), previous: Math.round(2_210 * Math.sqrt(scale)), format: \"number\", spark: spark(1, 0.12) },\n      { id: \"conversion\", label: \"Conversion\", value: 3.84 + (seed % 5) * 0.07, previous: 3.62, format: \"percent\", spark: spark(2, 0.06) },\n      { id: \"churn\", label: \"Churn\", value: 1.92 - (seed % 3) * 0.08, previous: 2.31, format: \"percent\", lowerIsBetter: true, spark: spark(3, -0.1) },\n    ],\n    channels: [\n      { label: \"Direct\", value: Math.round(revenue * 0.34) },\n      { label: \"Organic search\", value: Math.round(revenue * 0.27) },\n      { label: \"Partner\", value: Math.round(revenue * 0.19) },\n      { label: \"Email\", value: Math.round(revenue * 0.12) },\n      { label: \"Paid social\", value: Math.round(revenue * 0.08) },\n    ],\n  };\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Formatting\n * -------------------------------------------------------------------------------------------------------- */\n\nfunction useFormatters(currency: string, locale: string) {\n  return useMemo(() => {\n    const money = new Intl.NumberFormat(locale, { style: \"currency\", currency, maximumFractionDigits: 0 });\n    const moneyCents = new Intl.NumberFormat(locale, { style: \"currency\", currency, minimumFractionDigits: 2 });\n    const compact = new Intl.NumberFormat(locale, { style: \"currency\", currency, notation: \"compact\", maximumFractionDigits: 1 });\n    const number = new Intl.NumberFormat(locale);\n    return {\n      kpi(value: number, format: DashboardKpi[\"format\"]) {\n        if (format === \"currency\") return money.format(value);\n        if (format === \"percent\") return `${value.toFixed(2)}%`;\n        return number.format(value);\n      },\n      money: (v: number) => money.format(v),\n      cents: (v: number) => moneyCents.format(v / 100),\n      compact: (v: number) => compact.format(v),\n    };\n  }, [currency, locale]);\n}\n\nfunction orderDate(iso: string) {\n  const [y, m, d] = iso.split(\"-\").map(Number);\n  return `${MONTHS[m - 1]} ${d}, ${y}`;\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Shell\n * -------------------------------------------------------------------------------------------------------- */\n\nconst RANGES: { id: DashboardRange; label: string; long: string }[] = [\n  { id: \"7d\", label: \"7D\", long: \"Last 7 days\" },\n  { id: \"30d\", label: \"30D\", long: \"Last 30 days\" },\n  { id: \"90d\", label: \"90D\", long: \"Last 90 days\" },\n  { id: \"12m\", label: \"12M\", long: \"Last 12 months\" },\n];\n\nconst DRAWER_BELOW = 768;\n\n/**\n * An application dashboard: a collapsible sidebar that turns into a drawer on narrow containers, a header with\n * search, a KPI row, a revenue chart with a table view, a channel breakdown and a sortable orders table.\n */\nexport function AppDashboardShell({\n  workspace = { name: \"Halcyon\", plan: \"Scale plan\" },\n  user = { name: \"Rhea Castillo\", email: \"rhea@halcyon.app\" },\n  nav = DASHBOARD_NAV,\n  defaultActive = \"overview\",\n  onNavigate,\n  orders = DASHBOARD_ORDERS,\n  getSeries = sampleDashboardSeries,\n  defaultRange = \"30d\",\n  onRangeChange,\n  defaultCollapsed = false,\n  currency = \"USD\",\n  locale = \"en-US\",\n  theme = \"auto\",\n  disableAnimation = false,\n  className,\n}: AppDashboardShellProps) {\n  const { style } = useAppBlockTheme(theme);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const width = useElementWidth(rootRef);\n  const narrow = width > 0 && width < DRAWER_BELOW;\n\n  const [collapsed, setCollapsed] = useState(defaultCollapsed);\n  const [drawerOpen, setDrawerOpen] = useState(false);\n  const [active, setActive] = useState(defaultActive);\n  const [range, setRange] = useState<DashboardRange>(defaultRange);\n  const [query, setQuery] = useState(\"\");\n  const searchRef = useRef<HTMLInputElement>(null);\n  const { announce, region } = useAnnouncer();\n  const fmt = useFormatters(currency, locale);\n\n  const series = useMemo(() => getSeries(range), [getSeries, range]);\n  const rangeLong = RANGES.find((r) => r.id === range)?.long ?? \"\";\n  const activeLabel = nav.flatMap((g) => g.items).find((i) => i.id === active)?.label ?? \"Overview\";\n\n  const navigate = (id: string) => {\n    setActive(id);\n    onNavigate?.(id);\n    setDrawerOpen(false);\n  };\n\n  const changeRange = (next: DashboardRange) => {\n    setRange(next);\n    onRangeChange?.(next);\n    announce(`Showing ${RANGES.find((r) => r.id === next)?.long.toLowerCase()}`);\n  };\n\n  // \"/\" focuses search from anywhere in the block, unless the user is already typing.\n  const onRootKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    if (event.key !== \"/\" || event.metaKey || event.ctrlKey || event.altKey) return;\n    const target = event.target as HTMLElement;\n    if (target.closest(\"input, textarea, select, [contenteditable=true]\")) return;\n    event.preventDefault();\n    searchRef.current?.focus();\n  };\n\n  const sidebar = (mode: \"rail\" | \"drawer\") => (\n    <SidebarContent\n      mode={mode}\n      collapsed={mode === \"rail\" && collapsed}\n      workspace={workspace}\n      user={user}\n      nav={nav}\n      active={active}\n      onNavigate={navigate}\n      onToggleCollapsed={() => setCollapsed((v) => !v)}\n      onClose={() => setDrawerOpen(false)}\n    />\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(blockRoot, \"h-full\", className)}\n      style={style}\n      onKeyDown={onRootKeyDown}\n    >\n      {!narrow && (\n        <aside\n          aria-label=\"Primary\"\n          className={cn(\n            \"relative z-20 flex h-full shrink-0 flex-col border-r border-[color:var(--bjork-border)] bg-[var(--bjork-panel)] transition-[width] duration-[260ms] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\",\n            collapsed ? \"w-[64px]\" : \"w-[244px]\",\n          )}\n        >\n          {sidebar(\"rail\")}\n        </aside>\n      )}\n\n      <div className=\"flex min-w-0 flex-1 flex-col\" inert={narrow && drawerOpen}>\n        <header className=\"flex h-14 shrink-0 items-center gap-2 border-b border-[color:var(--bjork-border)] px-3 @3xl:px-5\">\n          {narrow && (\n            <IconButton label=\"Open navigation\" onClick={() => setDrawerOpen(true)} aria-expanded={drawerOpen}>\n              <Menu />\n            </IconButton>\n          )}\n          <nav aria-label=\"Breadcrumb\" className=\"min-w-0\">\n            <ol className=\"flex min-w-0 items-center gap-1.5 text-[13px]\">\n              <li className=\"hidden truncate text-[color:var(--bjork-text-muted)] @md:block\">{workspace.name}</li>\n              <li aria-hidden=\"true\" className=\"hidden text-[color:var(--bjork-text-faint)] @md:block\">\n                /\n              </li>\n              <li aria-current=\"page\" className=\"truncate font-medium text-[color:var(--bjork-text)]\">\n                {activeLabel}\n              </li>\n            </ol>\n          </nav>\n\n          <div className=\"ml-auto flex items-center gap-1.5\">\n            <label className=\"group relative hidden items-center @2xl:flex\">\n              <span className=\"sr-only\">Search orders</span>\n              <Search aria-hidden=\"true\" className=\"pointer-events-none absolute left-2.5 size-[15px] text-[color:var(--bjork-text-soft)]\" />\n              <input\n                ref={searchRef}\n                type=\"search\"\n                value={query}\n                onChange={(e) => setQuery(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Escape\" && query) {\n                    e.stopPropagation();\n                    setQuery(\"\");\n                  }\n                }}\n                placeholder=\"Search orders\"\n                className=\"h-8 w-[220px] rounded-[9px] border border-[color:var(--bjork-border)] bg-[var(--bjork-field)] pl-8 pr-8 text-[13px] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)] outline-none transition-[border-color,box-shadow,width] duration-200 ease-out placeholder:text-[color:var(--bjork-text-soft)] focus:w-[260px] focus-visible:border-[color:var(--bjork-accent)] focus-visible:ring-[3px] focus-visible:ring-[color:var(--bjork-accent-soft)] motion-reduce:transition-none [&::-webkit-search-cancel-button]:hidden\"\n              />\n              <Kbd className=\"pointer-events-none absolute right-2 group-focus-within:opacity-0\">/</Kbd>\n            </label>\n            <IconButton\n              label=\"Search orders\"\n              className=\"@2xl:hidden\"\n              onClick={() => {\n                const el = rootRef.current?.querySelector<HTMLInputElement>(\"[data-mobile-search]\");\n                el?.focus();\n              }}\n            >\n              <Search />\n            </IconButton>\n            <IconButton label=\"Notifications, 3 unread\" className=\"relative\">\n              <Bell />\n              <span aria-hidden=\"true\" className=\"absolute right-2 top-2 size-1.5 rounded-full bg-[var(--bjork-accent)] ring-2 ring-[color:var(--bjork-bg)]\" />\n            </IconButton>\n            <BlockButton variant=\"secondary\" size=\"sm\" className=\"hidden @md:inline-flex\">\n              <Download aria-hidden=\"true\" />\n              Export\n            </BlockButton>\n          </div>\n        </header>\n\n        <main className=\"@container/main min-h-0 flex-1 overflow-y-auto overscroll-contain\">\n          <div className=\"mx-auto flex w-full max-w-[1280px] flex-col gap-4 px-3 pb-10 pt-5 @3xl:gap-5 @3xl:px-6 @3xl:pt-6\">\n            <div className=\"flex flex-wrap items-end justify-between gap-x-6 gap-y-3\">\n              <div className=\"min-w-0\">\n                <h1 className=\"font-bjork-display text-[24px] font-bold italic leading-none tracking-[-0.02em] @3xl:text-[28px]\">\n                  {activeLabel}\n                </h1>\n                <p className=\"mt-2 text-[13px] text-[color:var(--bjork-text-muted)]\">\n                  {rangeLong} · compared with the previous period\n                </p>\n              </div>\n              <RangeControl value={range} onChange={changeRange} />\n            </div>\n\n            <section aria-label=\"Key metrics\" className=\"grid grid-cols-2 gap-2.5 @3xl/main:gap-3 @5xl/main:grid-cols-4\">\n              {series.kpis.map((kpi) => (\n                <KpiCard key={kpi.id} kpi={kpi} format={fmt.kpi} />\n              ))}\n            </section>\n\n            <div className=\"grid grid-cols-1 gap-3 @5xl/main:grid-cols-[minmax(0,1fr)_300px]\">\n              <RevenueChart\n                key={range}\n                points={series.points}\n                rangeLabel={rangeLong}\n                money={fmt.money}\n                compact={fmt.compact}\n                animate={!disableAnimation}\n              />\n              <ChannelBreakdown channels={series.channels} money={fmt.compact} />\n            </div>\n\n            <OrdersTable\n              orders={orders}\n              query={query}\n              onQueryChange={setQuery}\n              cents={fmt.cents}\n              announce={announce}\n            />\n          </div>\n        </main>\n      </div>\n\n      {narrow && (\n        <BlockDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} label=\"Navigation\">\n          {sidebar(\"drawer\")}\n        </BlockDrawer>\n      )}\n      {region}\n    </div>\n  );\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Sidebar\n * -------------------------------------------------------------------------------------------------------- */\n\nfunction SidebarContent({\n  mode,\n  collapsed,\n  workspace,\n  user,\n  nav,\n  active,\n  onNavigate,\n  onToggleCollapsed,\n  onClose,\n}: {\n  mode: \"rail\" | \"drawer\";\n  collapsed: boolean;\n  workspace: { name: string; plan: string };\n  user: { name: string; email: string };\n  nav: DashboardNavGroup[];\n  active: string;\n  onNavigate: (id: string) => void;\n  onToggleCollapsed: () => void;\n  onClose: () => void;\n}) {\n  return (\n    <>\n      <div className={cn(\"flex h-14 shrink-0 items-center gap-2 border-b border-[color:var(--bjork-border)]\", collapsed ? \"justify-center px-2\" : \"px-3\")}>\n        <button\n          type=\"button\"\n          className={cn(\n            \"flex min-w-0 flex-1 items-center gap-2.5 rounded-[10px] p-1.5 text-left transition-colors duration-150 hover:bg-[var(--bjork-surface-hover)]\",\n            collapsed && \"flex-none\",\n            focusRing,\n          )}\n          aria-label={collapsed ? `Switch workspace, current ${workspace.name}` : undefined}\n          title={collapsed ? workspace.name : undefined}\n        >\n          <WorkspaceMark />\n          {!collapsed && (\n            <>\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"block truncate text-[13.5px] font-semibold leading-tight\">{workspace.name}</span>\n                <span className=\"block truncate text-[11.5px] leading-tight text-[color:var(--bjork-text-muted)]\">{workspace.plan}</span>\n              </span>\n              <ChevronsUpDown aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-[color:var(--bjork-text-soft)]\" />\n            </>\n          )}\n        </button>\n        {mode === \"drawer\" && (\n          <IconButton label=\"Close navigation\" size=\"sm\" onClick={onClose}>\n            <X />\n          </IconButton>\n        )}\n      </div>\n\n      <nav aria-label=\"Main\" className={cn(\"hide-scrollbar flex-1 overflow-y-auto py-3\", collapsed ? \"px-2\" : \"px-2.5\")}>\n        {nav.map((group, gi) => (\n          <div key={group.label ?? gi} className={cn(gi > 0 && \"mt-5\")}>\n            {group.label &&\n              (collapsed ? (\n                <div aria-hidden=\"true\" className=\"mx-auto mb-2 h-px w-6 bg-[var(--bjork-border)]\" />\n              ) : (\n                <h2 className=\"mb-1.5 px-2.5 font-mono text-[10.5px] uppercase tracking-[0.1em] text-[color:var(--bjork-text-soft)]\">\n                  {group.label}\n                </h2>\n              ))}\n            <ul className=\"flex flex-col gap-0.5\">\n              {group.items.map((item) => {\n                const isActive = item.id === active;\n                const Icon = item.icon;\n                return (\n                  <li key={item.id}>\n                    <a\n                      href={`#${item.id}`}\n                      onClick={(e) => {\n                        e.preventDefault();\n                        onNavigate(item.id);\n                      }}\n                      aria-current={isActive ? \"page\" : undefined}\n                      title={collapsed ? item.label : undefined}\n                      className={cn(\n                        \"group relative flex h-9 items-center gap-2.5 rounded-[9px] text-[13.5px] transition-colors duration-150\",\n                        collapsed ? \"justify-center px-0\" : \"px-2.5\",\n                        isActive\n                          ? \"bg-[var(--bjork-surface)] font-medium text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft),0_0_0_1px_var(--bjork-border)]\"\n                          : \"text-[color:var(--bjork-text-medium)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]\",\n                        focusRing,\n                      )}\n                    >\n                      {isActive && (\n                        <span aria-hidden=\"true\" className=\"absolute -left-2.5 top-2 h-5 w-[3px] rounded-r-full bg-[var(--bjork-accent)]\" />\n                      )}\n                      <Icon\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"size-[17px] shrink-0\",\n                          isActive ? \"text-[color:var(--blk-accent-ink)]\" : \"text-[color:var(--bjork-text-muted)] group-hover:text-[color:var(--bjork-text-medium)]\",\n                        )}\n                      />\n                      {collapsed ? (\n                        <span className=\"sr-only\">{item.label}</span>\n                      ) : (\n                        <>\n                          <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n                          {item.badge && (\n                            <span\n                              className={cn(\n                                \"rounded-[6px] px-1.5 py-px font-mono text-[11px] tabular-nums\",\n                                item.badge === \"New\"\n                                  ? \"bg-[var(--bjork-accent-soft)] text-[color:var(--blk-accent-ink)]\"\n                                  : \"bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text-muted)]\",\n                              )}\n                            >\n                              {item.badge}\n                            </span>\n                          )}\n                        </>\n                      )}\n                      {collapsed && item.badge && item.badge !== \"New\" && (\n                        <span aria-hidden=\"true\" className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-[var(--bjork-accent)]\" />\n                      )}\n                    </a>\n                  </li>\n                );\n              })}\n            </ul>\n          </div>\n        ))}\n      </nav>\n\n      <div className={cn(\"shrink-0 border-t border-[color:var(--bjork-border)] p-2.5\", collapsed && \"px-2\")}>\n        {!collapsed && (\n          <div className=\"mb-2.5 rounded-[12px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] p-3 shadow-[var(--bjork-shadow-soft)]\">\n            <div className=\"flex items-baseline justify-between text-[12px]\">\n              <span className=\"font-medium\">Event quota</span>\n              <span className=\"font-mono tabular-nums text-[color:var(--bjork-text-muted)]\">8.2M / 10M</span>\n            </div>\n            <div\n              role=\"meter\"\n              aria-label=\"Event quota used\"\n              aria-valuemin={0}\n              aria-valuemax={100}\n              aria-valuenow={82}\n              aria-valuetext=\"82 percent used\"\n              className=\"mt-2 h-1.5 overflow-hidden rounded-full bg-[var(--bjork-surface-active)]\"\n            >\n              <div className=\"h-full w-[82%] rounded-full bg-[var(--bjork-accent)]\" />\n            </div>\n            <p className=\"mt-2 text-[11.5px] leading-snug text-[color:var(--bjork-text-muted)]\">Resets in 23 days.</p>\n          </div>\n        )}\n        <div className={cn(\"flex items-center gap-1\", collapsed ? \"flex-col\" : \"\")}>\n          <button\n            type=\"button\"\n            className={cn(\n              \"flex min-w-0 items-center gap-2.5 rounded-[10px] p-1.5 text-left transition-colors duration-150 hover:bg-[var(--bjork-surface-hover)]\",\n              collapsed ? \"flex-none\" : \"flex-1\",\n              focusRing,\n            )}\n            aria-label={`Account: ${user.name}`}\n          >\n            <InitialsAvatar name={user.name} size={28} />\n            {!collapsed && (\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"block truncate text-[12.5px] font-medium leading-tight\">{user.name}</span>\n                <span className=\"block truncate text-[11.5px] leading-tight text-[color:var(--bjork-text-muted)]\">{user.email}</span>\n              </span>\n            )}\n          </button>\n          {!collapsed && (\n            <IconButton label=\"Help and support\" size=\"sm\">\n              <LifeBuoy />\n            </IconButton>\n          )}\n          {mode === \"rail\" && (\n            <IconButton label={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"} size=\"sm\" onClick={onToggleCollapsed} aria-expanded={!collapsed}>\n              {collapsed ? <PanelLeftOpen /> : <PanelLeftClose />}\n            </IconButton>\n          )}\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction WorkspaceMark() {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"relative grid size-8 shrink-0 place-items-center overflow-hidden rounded-[9px] bg-[var(--blk-accent-fill)] shadow-[inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.2)]\"\n    >\n      <svg viewBox=\"0 0 20 20\" className=\"size-[18px]\" fill=\"none\">\n        <path d=\"M4 15.5V4.5M16 15.5V4.5M4 10h12\" stroke=\"var(--blk-accent-fill-ink)\" strokeWidth=\"2.2\" strokeLinecap=\"round\" />\n        <circle cx=\"10\" cy=\"10\" r=\"2.1\" fill=\"var(--blk-accent-fill-ink)\" />\n      </svg>\n    </span>\n  );\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Range control: a radio group with roving focus.\n * -------------------------------------------------------------------------------------------------------- */\n\nfunction RangeControl({ value, onChange }: { value: DashboardRange; onChange: (range: DashboardRange) => void }) {\n  const refs = useRef<(HTMLButtonElement | null)[]>([]);\n  const index = RANGES.findIndex((r) => r.id === value);\n  const onKeyDown = (event: ReactKeyboardEvent) => {\n    let next = index;\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") next = (index + 1) % RANGES.length;\n    else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") next = (index - 1 + RANGES.length) % RANGES.length;\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = RANGES.length - 1;\n    else return;\n    event.preventDefault();\n    onChange(RANGES[next].id);\n    refs.current[next]?.focus();\n  };\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label=\"Date range\"\n      onKeyDown={onKeyDown}\n      className=\"relative inline-grid grid-cols-4 rounded-[11px] border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] p-[3px] shadow-[var(--bjork-shadow-inset)]\"\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute inset-y-[3px] left-[3px] w-[calc((100%-6px)/4)] rounded-[8px] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface),0_0_0_1px_var(--bjork-border)] transition-transform duration-[220ms] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\"\n        style={{ transform: `translateX(${index * 100}%)` }}\n      />\n      {RANGES.map((r, i) => (\n        <button\n          key={r.id}\n          ref={(el) => {\n            refs.current[i] = el;\n          }}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={r.id === value}\n          aria-label={r.long}\n          tabIndex={r.id === value ? 0 : -1}\n          onClick={() => onChange(r.id)}\n          className={cn(\n            \"relative z-10 h-7 min-w-[46px] cursor-pointer rounded-[8px] px-2.5 font-mono text-[11.5px] font-medium tabular-nums transition-colors duration-150\",\n            r.id === value ? \"text-[color:var(--bjork-text)]\" : \"text-[color:var(--bjork-text-muted)] hover:text-[color:var(--bjork-text)]\",\n            focusRing,\n          )}\n        >\n          {r.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * KPI card\n * -------------------------------------------------------------------------------------------------------- */\n\nfunction KpiCard({ kpi, format }: { kpi: DashboardKpi; format: (v: number, f: DashboardKpi[\"format\"]) => string }) {\n  const change = kpi.previous === 0 ? 0 : ((kpi.value - kpi.previous) / kpi.previous) * 100;\n  const up = change >= 0;\n  const good = kpi.lowerIsBetter ? !up : up;\n  const Arrow = up ? ArrowUpRight : ArrowDownRight;\n  const changeText = `${up ? \"+\" : \"−\"}${Math.abs(change).toFixed(1)}%`;\n  return (\n    <article className=\"@container/kpi relative flex min-w-0 flex-col overflow-hidden rounded-[16px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] p-3.5 shadow-[var(--bjork-shadow-surface)] @3xl/main:p-4\">\n      <h3 className=\"text-[12.5px] font-medium text-[color:var(--bjork-text-muted)]\">{kpi.label}</h3>\n      <div className=\"mt-3 flex items-end justify-between gap-3\">\n        <div className=\"min-w-0\">\n          <p className=\"truncate text-[21px] font-semibold leading-none tracking-[-0.03em] tabular-nums @[220px]/kpi:text-[26px]\">{format(kpi.value, kpi.format)}</p>\n          <p className=\"mt-2.5 flex items-center gap-1.5 text-[12px]\">\n            <span\n              className={cn(\n                \"inline-flex items-center gap-0.5 rounded-[6px] px-1.5 py-[2px] font-mono text-[11px] font-medium tabular-nums\",\n                good\n                  ? \"bg-[var(--blk-success-soft)] text-[color:var(--blk-success)]\"\n                  : \"bg-[var(--blk-error-soft)] text-[color:var(--blk-error)]\",\n              )}\n            >\n              <Arrow aria-hidden=\"true\" className=\"size-3\" strokeWidth={2.25} />\n              {changeText}\n            </span>\n            <span className=\"hidden text-[color:var(--bjork-text-soft)] @[200px]/kpi:inline\">vs prior</span>\n            <span className=\"sr-only\">{good ? \"(improving)\" : \"(worsening)\"}</span>\n          </p>\n        </div>\n        <Sparkline values={kpi.spark} good={good} />\n      </div>\n    </article>\n  );\n}\n\nfunction Sparkline({ values, good }: { values: number[]; good: boolean }) {\n  const w = 76;\n  const h = 30;\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  const pts = values.map((v, i) => [\n    (i / (values.length - 1)) * (w - 4) + 2,\n    h - 3 - ((v - min) / (max - min || 1)) * (h - 6),\n  ]);\n  const d = smoothPath(pts);\n  const last = pts[pts.length - 1];\n  const gid = useId();\n  return (\n    <svg aria-hidden=\"true\" width={w} height={h} viewBox={`0 0 ${w} ${h}`} className=\"hidden shrink-0 overflow-visible @[230px]/kpi:block\">\n      <defs>\n        <linearGradient id={gid} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor=\"var(--bjork-text)\" stopOpacity=\"0.1\" />\n          <stop offset=\"1\" stopColor=\"var(--bjork-text)\" stopOpacity=\"0\" />\n        </linearGradient>\n      </defs>\n      <path d={`${d} L ${last[0]} ${h} L ${pts[0][0]} ${h} Z`} fill={`url(#${gid})`} />\n      <path d={d} fill=\"none\" stroke=\"var(--bjork-text-soft)\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      <circle cx={last[0]} cy={last[1]} r=\"2.75\" fill={good ? \"var(--blk-success)\" : \"var(--blk-error)\"} stroke=\"var(--bjork-surface)\" strokeWidth=\"1.5\" />\n    </svg>\n  );\n}\n\n// Monotone cubic interpolation, so the curve never overshoots the data.\nfunction smoothPath(pts: number[][]) {\n  if (pts.length < 2) return \"\";\n  const n = pts.length;\n  const dx: number[] = [];\n  const slope: number[] = [];\n  for (let i = 0; i < n - 1; i += 1) {\n    dx.push(pts[i + 1][0] - pts[i][0]);\n    slope.push((pts[i + 1][1] - pts[i][1]) / (dx[i] || 1));\n  }\n  const m: number[] = [slope[0]];\n  for (let i = 1; i < n - 1; i += 1) {\n    m.push(slope[i - 1] * slope[i] <= 0 ? 0 : (3 * (dx[i - 1] + dx[i])) / ((2 * dx[i] + dx[i - 1]) / slope[i - 1] + (dx[i] + 2 * dx[i - 1]) / slope[i]));\n  }\n  m.push(slope[n - 2]);\n  let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;\n  for (let i = 0; i < n - 1; i += 1) {\n    const [x0, y0] = pts[i];\n    const [x1, y1] = pts[i + 1];\n    const h = dx[i] / 3;\n    d += ` C ${(x0 + h).toFixed(2)} ${(y0 + m[i] * h).toFixed(2)}, ${(x1 - h).toFixed(2)} ${(y1 - m[i + 1] * h).toFixed(2)}, ${x1.toFixed(2)} ${y1.toFixed(2)}`;\n  }\n  return d;\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Revenue chart: current period area, previous period dashed, crosshair tooltip, keyboard stepping and a\n * table view.\n * -------------------------------------------------------------------------------------------------------- */\n\nconst CHART_H = 236;\nconst PAD = { top: 16, right: 12, bottom: 28, left: 52 };\n\nfunction niceMax(value: number) {\n  const exp = Math.pow(10, Math.floor(Math.log10(value)));\n  const f = value / exp;\n  const nice = f <= 1 ? 1 : f <= 1.5 ? 1.5 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 3 ? 3 : f <= 4 ? 4 : f <= 5 ? 5 : f <= 6 ? 6 : f <= 8 ? 8 : 10;\n  return nice * exp;\n}\n\nfunction RevenueChart({\n  points,\n  rangeLabel,\n  money,\n  compact,\n  animate,\n}: {\n  points: DashboardPoint[];\n  rangeLabel: string;\n  money: (v: number) => string;\n  compact: (v: number) => string;\n  animate: boolean;\n}) {\n  const [view, setView] = useState<\"chart\" | \"table\">(\"chart\");\n  const [hover, setHover] = useState<number | null>(null);\n  const plotRef = useRef<HTMLDivElement>(null);\n  const width = useElementWidth(plotRef);\n  const reduce = useBlockReducedMotion();\n  const [revealed, setRevealed] = useState(!animate);\n  const titleId = useId();\n  const descId = useId();\n  const gradId = useId();\n  const clipId = useId();\n\n  useEffect(() => {\n    if (!animate || reduce) return;\n    const id = window.requestAnimationFrame(() => setRevealed(true));\n    return () => window.cancelAnimationFrame(id);\n  }, [animate, reduce]);\n  const shown = revealed || reduce;\n\n  const total = points.reduce((s, p) => s + p.current, 0);\n  const totalPrev = points.reduce((s, p) => s + p.previous, 0);\n  const max = niceMax(Math.max(...points.map((p) => Math.max(p.current, p.previous))) * 1.08);\n  const w = Math.max(width, 240);\n  const innerW = w - PAD.left - PAD.right;\n  const innerH = CHART_H - PAD.top - PAD.bottom;\n  const x = useCallback((i: number) => PAD.left + (points.length === 1 ? innerW / 2 : (i / (points.length - 1)) * innerW), [innerW, points.length]);\n  const y = (v: number) => PAD.top + innerH - (v / max) * innerH;\n\n  const cur = points.map((p, i) => [x(i), y(p.current)]);\n  const prev = points.map((p, i) => [x(i), y(p.previous)]);\n  const curD = smoothPath(cur);\n  const prevD = smoothPath(prev);\n  const areaD = `${curD} L ${x(points.length - 1)} ${PAD.top + innerH} L ${x(0)} ${PAD.top + innerH} Z`;\n  const ticks = [0, 0.25, 0.5, 0.75, 1].map((t) => t * max);\n  const labelEvery = Math.ceil(points.length / Math.max(2, Math.floor(innerW / 64)));\n\n  const pickIndex = (clientX: number) => {\n    const rect = plotRef.current?.getBoundingClientRect();\n    if (!rect) return null;\n    const px = clientX - rect.left - PAD.left;\n    return Math.max(0, Math.min(points.length - 1, Math.round((px / innerW) * (points.length - 1))));\n  };\n\n  const onKeyDown = (event: ReactKeyboardEvent) => {\n    const last = points.length - 1;\n    const at = hover ?? last;\n    let next: number | null = null;\n    if (event.key === \"ArrowRight\") next = Math.min(last, hover === null ? last : at + 1);\n    else if (event.key === \"ArrowLeft\") next = Math.max(0, hover === null ? last : at - 1);\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = last;\n    else if (event.key === \"Escape\") {\n      setHover(null);\n      return;\n    } else return;\n    event.preventDefault();\n    setHover(next);\n  };\n\n  const active = hover !== null ? points[hover] : null;\n  const delta = active ? ((active.current - active.previous) / active.previous) * 100 : 0;\n  const tipLeft = hover !== null ? x(hover) : 0;\n  const tipFlip = tipLeft > w - 190;\n  const change = ((total - totalPrev) / totalPrev) * 100;\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      className=\"flex min-w-0 flex-col rounded-[16px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\"\n    >\n      <div className=\"flex flex-wrap items-start justify-between gap-3 px-4 pt-4\">\n        <div className=\"min-w-0\">\n          <h2 id={titleId} className=\"text-[13.5px] font-semibold\">\n            Revenue\n          </h2>\n          <p className=\"mt-1 flex flex-wrap items-baseline gap-x-2 text-[12.5px] text-[color:var(--bjork-text-muted)]\">\n            <span className=\"text-[19px] font-semibold tracking-[-0.025em] tabular-nums text-[color:var(--bjork-text)]\">{money(total)}</span>\n            <span className=\"tabular-nums\">\n              {change >= 0 ? \"+\" : \"−\"}\n              {Math.abs(change).toFixed(1)}% vs prior period\n            </span>\n          </p>\n        </div>\n        <div className=\"flex items-center gap-3\">\n          <ul className=\"hidden items-center gap-3 text-[11.5px] text-[color:var(--bjork-text-muted)] @md/main:flex\" aria-label=\"Legend\">\n            <li className=\"flex items-center gap-1.5\">\n              <span aria-hidden=\"true\" className=\"h-[2px] w-3.5 rounded-full bg-[var(--bjork-accent)]\" />\n              This period\n            </li>\n            <li className=\"flex items-center gap-1.5\">\n              <svg aria-hidden=\"true\" width=\"14\" height=\"2\" className=\"overflow-visible\">\n                <line x1=\"0\" x2=\"14\" y1=\"1\" y2=\"1\" stroke=\"var(--blk-chart-ghost)\" strokeWidth=\"1.5\" strokeDasharray=\"3 3\" />\n              </svg>\n              Prior period\n            </li>\n          </ul>\n          <div className=\"flex rounded-[9px] border border-[color:var(--bjork-border)] p-[2px]\" role=\"group\" aria-label=\"View\">\n            <IconButton label=\"Chart view\" size=\"sm\" pressed={view === \"chart\"} onClick={() => setView(\"chart\")} className=\"size-6 rounded-[7px] aria-pressed:bg-[var(--bjork-surface-active)]\">\n              <ChartArea />\n            </IconButton>\n            <IconButton label=\"Table view\" size=\"sm\" pressed={view === \"table\"} onClick={() => setView(\"table\")} className=\"size-6 rounded-[7px] aria-pressed:bg-[var(--bjork-surface-active)]\">\n              <Table2 />\n            </IconButton>\n          </div>\n        </div>\n      </div>\n\n      {view === \"chart\" ? (\n        <div\n          ref={plotRef}\n          role=\"img\"\n          tabIndex={0}\n          aria-labelledby={titleId}\n          aria-describedby={descId}\n          onKeyDown={onKeyDown}\n          onPointerMove={(e) => setHover(pickIndex(e.clientX))}\n          onPointerLeave={() => setHover(null)}\n          onBlur={() => setHover(null)}\n          className={cn(\"relative mx-1 mb-1 mt-2 cursor-crosshair rounded-[12px] outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)]\")}\n          style={{ height: CHART_H }}\n        >\n          <p id={descId} className=\"sr-only\">\n            {rangeLabel}: {money(total)} in revenue against {money(totalPrev)} in the prior period. Use the left and right\n            arrow keys to read each point.\n          </p>\n          {width > 0 && (\n            <svg width={w} height={CHART_H} className=\"block overflow-visible\" aria-hidden=\"true\">\n              <defs>\n                <linearGradient id={gradId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                  <stop offset=\"0\" stopColor=\"var(--bjork-accent)\" stopOpacity=\"0.22\" />\n                  <stop offset=\"1\" stopColor=\"var(--bjork-accent)\" stopOpacity=\"0\" />\n                </linearGradient>\n                <clipPath id={clipId}>\n                  <rect\n                    x={0}\n                    y={0}\n                    width={w}\n                    height={CHART_H}\n                    style={{\n                      transformOrigin: `${PAD.left}px 0`,\n                      transform: shown ? \"scaleX(1)\" : \"scaleX(0)\",\n                      transition: animate && !reduce ? \"transform 700ms cubic-bezier(0.23,1,0.32,1)\" : \"none\",\n                    }}\n                  />\n                </clipPath>\n              </defs>\n              {ticks.map((t) => (\n                <g key={t}>\n                  <line\n                    x1={PAD.left}\n                    x2={w - PAD.right}\n                    y1={y(t)}\n                    y2={y(t)}\n                    stroke=\"var(--bjork-border)\"\n                    strokeDasharray={t === 0 ? undefined : \"2 4\"}\n                  />\n                  <text x={PAD.left - 10} y={y(t)} dy=\"0.32em\" textAnchor=\"end\" className=\"fill-[color:var(--bjork-text-soft)] font-mono text-[11px] tabular-nums\">\n                    {compact(t)}\n                  </text>\n                </g>\n              ))}\n              {points.map((p, i) => {\n                const last = points.length - 1;\n                // Every nth tick, plus the last point; drop the nth tick that would crowd it.\n                const show = i === last || (i % labelEvery === 0 && last - i >= labelEvery * 0.7);\n                if (!show) return null;\n                return (\n                  <text\n                    key={p.label}\n                    x={x(i)}\n                    y={CHART_H - 8}\n                    textAnchor={i === 0 ? \"start\" : i === last ? \"end\" : \"middle\"}\n                    className=\"fill-[color:var(--bjork-text-soft)] font-mono text-[11px]\"\n                  >\n                    {p.tick}\n                  </text>\n                );\n              })}\n              <g clipPath={`url(#${clipId})`}>\n                <path d={prevD} fill=\"none\" stroke=\"var(--blk-chart-ghost)\" strokeWidth=\"1.5\" strokeDasharray=\"4 4\" strokeLinecap=\"round\" />\n                <path d={areaD} fill={`url(#${gradId})`} />\n                <path d={curD} fill=\"none\" stroke=\"var(--bjork-accent)\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n              </g>\n              {hover !== null && (\n                <g>\n                  <line x1={x(hover)} x2={x(hover)} y1={PAD.top} y2={PAD.top + innerH} stroke=\"var(--bjork-border-strong)\" />\n                  <circle cx={x(hover)} cy={y(points[hover].previous)} r=\"3.5\" fill=\"var(--bjork-surface)\" stroke=\"var(--blk-chart-ghost)\" strokeWidth=\"1.5\" />\n                  <circle cx={x(hover)} cy={y(points[hover].current)} r=\"4.5\" fill=\"var(--bjork-accent)\" stroke=\"var(--bjork-surface)\" strokeWidth=\"2\" />\n                </g>\n              )}\n            </svg>\n          )}\n          {active && (\n            <div\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute top-2 z-10 w-[176px] rounded-[12px] border border-[color:var(--bjork-border)] bg-[var(--bjork-menu)] p-2.5 shadow-[var(--bjork-shadow-menu)] backdrop-blur-xl\"\n              style={{ left: tipFlip ? tipLeft - 176 - 12 : tipLeft + 12 }}\n            >\n              <p className=\"text-[11.5px] font-medium text-[color:var(--bjork-text-muted)]\">{active.label}</p>\n              <dl className=\"mt-1.5 space-y-1 text-[12px]\">\n                <div className=\"flex items-center justify-between gap-2\">\n                  <dt className=\"flex items-center gap-1.5 text-[color:var(--bjork-text-medium)]\">\n                    <span className=\"h-[2px] w-2.5 rounded-full bg-[var(--bjork-accent)]\" />\n                    This period\n                  </dt>\n                  <dd className=\"font-mono tabular-nums\">{money(active.current)}</dd>\n                </div>\n                <div className=\"flex items-center justify-between gap-2\">\n                  <dt className=\"flex items-center gap-1.5 text-[color:var(--bjork-text-medium)]\">\n                    <span className=\"h-[2px] w-2.5 rounded-full bg-[var(--blk-chart-ghost)]\" />\n                    Prior\n                  </dt>\n                  <dd className=\"font-mono tabular-nums text-[color:var(--bjork-text-muted)]\">{money(active.previous)}</dd>\n                </div>\n              </dl>\n              <p className={cn(\"mt-1.5 border-t border-[color:var(--bjork-border)] pt-1.5 font-mono text-[11px] tabular-nums\", delta >= 0 ? \"text-[color:var(--blk-success)]\" : \"text-[color:var(--blk-error)]\")}>\n                {delta >= 0 ? \"+\" : \"−\"}\n                {Math.abs(delta).toFixed(1)}%\n              </p>\n            </div>\n          )}\n          <span className=\"sr-only\" aria-live=\"polite\">\n            {active ? `${active.label}: ${money(active.current)}, prior ${money(active.previous)}` : \"\"}\n          </span>\n        </div>\n      ) : (\n        <div className=\"mx-1 mb-1 mt-2 max-h-[236px] overflow-auto rounded-[12px]\">\n          <table className=\"w-full text-[12.5px]\">\n            <caption className=\"sr-only\">Revenue, {rangeLabel.toLowerCase()}</caption>\n            <thead className=\"sticky top-0 bg-[var(--bjork-surface)]\">\n              <tr className=\"text-left text-[11px] uppercase tracking-[0.06em] text-[color:var(--bjork-text-soft)]\">\n                <th scope=\"col\" className=\"px-3 py-2 font-medium\">Period</th>\n                <th scope=\"col\" className=\"px-3 py-2 text-right font-medium\">This period</th>\n                <th scope=\"col\" className=\"px-3 py-2 text-right font-medium\">Prior</th>\n              </tr>\n            </thead>\n            <tbody>\n              {points.map((p) => (\n                <tr key={p.label} className=\"border-t border-[color:var(--bjork-border)]\">\n                  <th scope=\"row\" className=\"px-3 py-1.5 text-left font-normal text-[color:var(--bjork-text-medium)]\">{p.label}</th>\n                  <td className=\"px-3 py-1.5 text-right font-mono tabular-nums\">{money(p.current)}</td>\n                  <td className=\"px-3 py-1.5 text-right font-mono tabular-nums text-[color:var(--bjork-text-muted)]\">{money(p.previous)}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      )}\n    </section>\n  );\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Channel breakdown\n * -------------------------------------------------------------------------------------------------------- */\n\nfunction ChannelBreakdown({ channels, money }: { channels: { label: string; value: number }[]; money: (v: number) => string }) {\n  const total = channels.reduce((s, c) => s + c.value, 0);\n  const max = Math.max(...channels.map((c) => c.value));\n  const titleId = useId();\n  return (\n    <section\n      aria-labelledby={titleId}\n      className=\"flex min-w-0 flex-col rounded-[16px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] p-4 shadow-[var(--bjork-shadow-surface)]\"\n    >\n      <div className=\"flex items-baseline justify-between\">\n        <h2 id={titleId} className=\"text-[13.5px] font-semibold\">\n          Revenue by channel\n        </h2>\n        <span className=\"font-mono text-[11.5px] tabular-nums text-[color:var(--bjork-text-muted)]\">{money(total)}</span>\n      </div>\n      <ul className=\"mt-4 flex flex-1 flex-col justify-between gap-3.5\">\n        {channels.map((c, i) => {\n          const share = (c.value / total) * 100;\n          return (\n            <li key={c.label}>\n              <div className=\"flex items-baseline justify-between gap-2 text-[12.5px]\">\n                <span className=\"truncate text-[color:var(--bjork-text-medium)]\">{c.label}</span>\n                <span className=\"shrink-0 font-mono text-[11.5px] tabular-nums\">\n                  {money(c.value)}\n                  <span className=\"ml-1.5 inline-block w-[3.2ch] text-right text-[color:var(--bjork-text-soft)]\">{Math.round(share)}%</span>\n                </span>\n              </div>\n              <div aria-hidden=\"true\" className=\"mt-1.5 h-1.5 overflow-hidden rounded-full bg-[var(--bjork-surface-active)]\">\n                <div\n                  className=\"h-full rounded-full\"\n                  style={{\n                    width: `${(c.value / max) * 100}%`,\n                    background: \"var(--bjork-accent)\",\n                    opacity: 1 - i * 0.14,\n                  }}\n                />\n              </div>\n            </li>\n          );\n        })}\n      </ul>\n    </section>\n  );\n}\n\n/* ----------------------------------------------------------------------------------------------------------\n * Orders table\n * -------------------------------------------------------------------------------------------------------- */\n\nconst STATUS: Record<OrderStatus, { label: string; ink: string; soft: string }> = {\n  paid: { label: \"Paid\", ink: \"var(--blk-success)\", soft: \"var(--blk-success-soft)\" },\n  pending: { label: \"Pending\", ink: \"var(--blk-warning)\", soft: \"var(--blk-warning-soft)\" },\n  refunded: { label: \"Refunded\", ink: \"var(--bjork-text-muted)\", soft: \"var(--bjork-surface-active)\" },\n  failed: { label: \"Failed\", ink: \"var(--blk-error)\", soft: \"var(--blk-error-soft)\" },\n};\n\ntype SortKey = \"id\" | \"customer\" | \"date\" | \"amount\";\nconst PAGE_SIZE = 6;\n\nfunction OrdersTable({\n  orders,\n  query,\n  onQueryChange,\n  cents,\n  announce,\n}: {\n  orders: DashboardOrder[];\n  query: string;\n  onQueryChange: (q: string) => void;\n  cents: (v: number) => string;\n  announce: (message: string) => void;\n}) {\n  const [filter, setFilter] = useState<OrderStatus | \"all\">(\"all\");\n  const [sort, setSort] = useState<{ key: SortKey; dir: \"asc\" | \"desc\" }>({ key: \"date\", dir: \"desc\" });\n  const [page, setPage] = useState(0);\n  const titleId = useId();\n\n  const rows = useMemo(() => {\n    const q = query.trim().toLowerCase();\n    const filtered = orders.filter(\n      (o) =>\n        (filter === \"all\" || o.status === filter) &&\n        (!q || o.customer.toLowerCase().includes(q) || o.id.toLowerCase().includes(q) || o.email.toLowerCase().includes(q)),\n    );\n    const dir = sort.dir === \"asc\" ? 1 : -1;\n    return [...filtered].sort((a, b) => {\n      const av = a[sort.key];\n      const bv = b[sort.key];\n      if (typeof av === \"number\" && typeof bv === \"number\") return (av - bv) * dir;\n      return String(av).localeCompare(String(bv)) * dir || a.id.localeCompare(b.id) * -1;\n    });\n  }, [orders, filter, query, sort]);\n\n  const pages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));\n  const current = Math.min(page, pages - 1);\n  const visible = rows.slice(current * PAGE_SIZE, current * PAGE_SIZE + PAGE_SIZE);\n\n  // Back to the first page whenever the result set changes shape.\n  const shapeKey = `${filter}|${query}|${sort.key}|${sort.dir}`;\n  const [lastShape, setLastShape] = useState(shapeKey);\n  if (lastShape !== shapeKey) {\n    setLastShape(shapeKey);\n    setPage(0);\n  }\n\n  const counts = useMemo(() => {\n    const c: Record<string, number> = { all: orders.length };\n    for (const o of orders) c[o.status] = (c[o.status] ?? 0) + 1;\n    return c;\n  }, [orders]);\n\n  const toggleSort = (key: SortKey) => {\n    const next = { key, dir: sort.key === key && sort.dir === \"desc\" ? (\"asc\" as const) : (\"desc\" as const) };\n    setSort(next);\n    announce(`Sorted by ${key}, ${next.dir === \"asc\" ? \"ascending\" : \"descending\"}`);\n  };\n\n  const header = (key: SortKey, label: string, className?: string) => {\n    const isSorted = sort.key === key;\n    const Icon = !isSorted ? ArrowUpDown : sort.dir === \"asc\" ? ArrowUp : ArrowDown;\n    return (\n      <th scope=\"col\" aria-sort={isSorted ? (sort.dir === \"asc\" ? \"ascending\" : \"descending\") : \"none\"} className={cn(\"px-3 py-0 font-medium\", className)}>\n        <button\n          type=\"button\"\n          onClick={() => toggleSort(key)}\n          className={cn(\n            \"inline-flex h-9 cursor-pointer items-center gap-1 rounded-[6px] uppercase tracking-[0.06em] transition-colors hover:text-[color:var(--bjork-text)]\",\n            isSorted && \"text-[color:var(--bjork-text-medium)]\",\n            focusRing,\n          )}\n        >\n          {label}\n          <Icon aria-hidden=\"true\" className={cn(\"size-3\", !isSorted && \"opacity-50\")} />\n        </button>\n      </th>\n    );\n  };\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      className=\"min-w-0 rounded-[16px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\"\n    >\n      <div className=\"flex flex-wrap items-center justify-between gap-3 px-4 pb-3 pt-4\">\n        <div className=\"flex items-baseline gap-2\">\n          <h2 id={titleId} className=\"text-[13.5px] font-semibold\">\n            Recent orders\n          </h2>\n          <span className=\"font-mono text-[11.5px] tabular-nums text-[color:var(--bjork-text-soft)]\">{rows.length}</span>\n        </div>\n        <div className=\"hide-scrollbar -mx-1 flex max-w-full items-center gap-1 overflow-x-auto px-1\" role=\"group\" aria-label=\"Filter by status\">\n          {([\"all\", \"paid\", \"pending\", \"refunded\", \"failed\"] as const).map((s) => (\n            <button\n              key={s}\n              type=\"button\"\n              aria-pressed={filter === s}\n              onClick={() => setFilter(s)}\n              className={cn(\n                \"inline-flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 text-[12px] transition-colors duration-150\",\n                filter === s\n                  ? \"border-[color:var(--bjork-border-strong)] bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text)]\"\n                  : \"border-transparent text-[color:var(--bjork-text-muted)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]\",\n                focusRing,\n              )}\n            >\n              {s !== \"all\" && <span aria-hidden=\"true\" className=\"size-1.5 rounded-full\" style={{ background: STATUS[s].ink }} />}\n              {s === \"all\" ? \"All\" : STATUS[s].label}\n              <span className=\"font-mono text-[11px] tabular-nums text-[color:var(--bjork-text-soft)]\">{counts[s] ?? 0}</span>\n            </button>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"px-4 pb-3 @2xl/main:hidden\">\n        <label className=\"relative flex items-center\">\n          <span className=\"sr-only\">Search orders</span>\n          <Search aria-hidden=\"true\" className=\"pointer-events-none absolute left-2.5 size-[15px] text-[color:var(--bjork-text-soft)]\" />\n          <input\n            data-mobile-search\n            type=\"search\"\n            value={query}\n            onChange={(e) => onQueryChange(e.target.value)}\n            placeholder=\"Search by name or order\"\n            className=\"h-9 w-full rounded-[9px] border border-[color:var(--bjork-border)] bg-[var(--bjork-field)] pl-8 pr-3 text-[13px] outline-none placeholder:text-[color:var(--bjork-text-soft)] focus-visible:border-[color:var(--bjork-accent)] focus-visible:ring-[3px] focus-visible:ring-[color:var(--bjork-accent-soft)]\"\n          />\n        </label>\n      </div>\n\n      <div className=\"overflow-x-auto\">\n        <table className=\"w-full min-w-0 border-collapse text-[13px]\">\n          <caption className=\"sr-only\">Recent orders, sortable by column</caption>\n          <thead>\n            <tr className=\"border-y border-[color:var(--bjork-border)] bg-[var(--bjork-panel)] text-left text-[11px] text-[color:var(--bjork-text-soft)]\">\n              {header(\"id\", \"Order\", \"hidden @2xl/main:table-cell\")}\n              {header(\"customer\", \"Customer\")}\n              <th scope=\"col\" className=\"px-3 font-medium uppercase tracking-[0.06em]\">Status</th>\n              <th scope=\"col\" className=\"hidden px-3 font-medium uppercase tracking-[0.06em] @4xl/main:table-cell\">Channel</th>\n              {header(\"date\", \"Date\", \"hidden @3xl/main:table-cell\")}\n              {header(\"amount\", \"Amount\", \"text-right [&>button]:flex-row-reverse\")}\n            </tr>\n          </thead>\n          <tbody>\n            {visible.map((o) => (\n              <tr key={o.id} className=\"group border-b border-[color:var(--bjork-border)] transition-colors duration-100 last:border-b-0 hover:bg-[var(--bjork-surface-hover)]\">\n                <td className=\"hidden px-3 py-2.5 font-mono text-[12px] tabular-nums text-[color:var(--bjork-text-muted)] @2xl/main:table-cell\">{o.id}</td>\n                <td className=\"max-w-0 px-3 py-2.5 @2xl/main:max-w-none\">\n                  <div className=\"flex min-w-0 items-center gap-2.5\">\n                    <InitialsAvatar name={o.customer} size={26} />\n                    <div className=\"min-w-0\">\n                      <p className=\"truncate font-medium leading-tight\">{o.customer}</p>\n                      <p className=\"truncate text-[11.5px] leading-tight text-[color:var(--bjork-text-muted)]\">\n                        <span className=\"@2xl/main:hidden\">{o.id} · </span>\n                        {o.email}\n                      </p>\n                    </div>\n                  </div>\n                </td>\n                <td className=\"px-3 py-2.5\">\n                  <span className=\"inline-flex items-center gap-1.5 rounded-full px-2 py-[3px] text-[11.5px] font-medium\" style={{ background: STATUS[o.status].soft, color: STATUS[o.status].ink }}>\n                    <StatusGlyph status={o.status} />\n                    {STATUS[o.status].label}\n                  </span>\n                </td>\n                <td className=\"hidden px-3 py-2.5 text-[color:var(--bjork-text-medium)] @4xl/main:table-cell\">{o.channel}</td>\n                <td className=\"hidden whitespace-nowrap px-3 py-2.5 tabular-nums text-[color:var(--bjork-text-medium)] @3xl/main:table-cell\">{orderDate(o.date)}</td>\n                <td className={cn(\"whitespace-nowrap px-3 py-2.5 text-right font-mono tabular-nums\", o.status === \"refunded\" && \"text-[color:var(--bjork-text-muted)] line-through decoration-[color:var(--bjork-text-faint)]\")}>\n                  {cents(o.amount)}\n                </td>\n              </tr>\n            ))}\n            {visible.length === 0 && (\n              <tr>\n                <td colSpan={6} className=\"px-4 py-10 text-center\">\n                  <p className=\"text-[13px] font-medium\">No orders match</p>\n                  <p className=\"mt-1 text-[12.5px] text-[color:var(--bjork-text-muted)]\">\n                    Try another name or clear the filters.\n                  </p>\n                  <BlockButton\n                    size=\"sm\"\n                    variant=\"secondary\"\n                    className=\"mt-3\"\n                    onClick={() => {\n                      setFilter(\"all\");\n                      onQueryChange(\"\");\n                    }}\n                  >\n                    Clear filters\n                  </BlockButton>\n                </td>\n              </tr>\n            )}\n          </tbody>\n        </table>\n      </div>\n\n      <div className=\"flex items-center justify-between gap-3 border-t border-[color:var(--bjork-border)] px-4 py-2.5 text-[12px] text-[color:var(--bjork-text-muted)]\">\n        <span className=\"tabular-nums\">\n          {rows.length === 0 ? \"0 results\" : `${current * PAGE_SIZE + 1}–${Math.min(rows.length, (current + 1) * PAGE_SIZE)} of ${rows.length}`}\n        </span>\n        <div className=\"flex items-center gap-1\">\n          <IconButton label=\"Previous page\" size=\"sm\" disabled={current === 0} onClick={() => setPage(current - 1)}>\n            <ChevronLeft />\n          </IconButton>\n          <span className=\"min-w-[4.5ch] text-center font-mono tabular-nums\" aria-live=\"polite\">\n            <span className=\"sr-only\">Page </span>\n            {current + 1}/{pages}\n          </span>\n          <IconButton label=\"Next page\" size=\"sm\" disabled={current >= pages - 1} onClick={() => setPage(current + 1)}>\n            <ChevronRight />\n          </IconButton>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction StatusGlyph({ status }: { status: OrderStatus }): ReactNode {\n  const common = { width: 10, height: 10, viewBox: \"0 0 10 10\", \"aria-hidden\": true, fill: \"none\", stroke: \"currentColor\", strokeWidth: 1.6, strokeLinecap: \"round\" as const };\n  if (status === \"paid\") return <svg {...common}><path d=\"M2 5.2 4.1 7.2 8 2.8\" strokeLinejoin=\"round\" /></svg>;\n  if (status === \"pending\") return <svg {...common}><circle cx=\"5\" cy=\"5\" r=\"3.6\" /><path d=\"M5 3.2V5l1.2.8\" /></svg>;\n  if (status === \"failed\") return <svg {...common}><path d=\"M2.8 2.8l4.4 4.4M7.2 2.8 2.8 7.2\" /></svg>;\n  return <svg {...common}><path d=\"M7.8 4.2A3 3 0 1 0 7.4 7M7.8 1.8v2.4H5.4\" strokeLinejoin=\"round\" /></svg>;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/blocks/app-dashboard-shell.tsx"
    },
    {
      "path": "components/bjork-ui/blocks/app-block-kit.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  type InputHTMLAttributes,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport type { BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { cn } from \"@/lib/utils\";\n\n// Shared plumbing for the application blocks (app-*). Every block is a full layout that sizes itself to its\n// container, not the viewport, so it behaves the same pasted into a page, a dialog or a docs preview.\n\nexport type AppBlockTheme = \"light\" | \"dark\" | \"auto\";\n\n// The library tokens, so a forced theme works even when the page itself is in the other mode.\nconst TOKENS: Record<BjorkTone, Record<string, string>> = {\n  light: {\n    \"--bjork-bg\": \"#f7f3ea\",\n    \"--bjork-surface\": \"#fffcf6\",\n    \"--bjork-surface-muted\": \"rgba(250, 246, 237, 0.94)\",\n    \"--bjork-surface-hover\": \"#f5efe3\",\n    \"--bjork-surface-active\": \"#efe7d8\",\n    \"--bjork-panel\": \"#f8f2e7\",\n    \"--bjork-menu\": \"rgba(255, 252, 246, 0.98)\",\n    \"--bjork-field\": \"rgba(255, 252, 246, 0.96)\",\n    \"--bjork-field-muted\": \"rgba(248, 242, 231, 0.82)\",\n    \"--bjork-field-inset\": \"rgba(239, 231, 216, 0.72)\",\n    \"--bjork-error-bg\": \"#fff0e8\",\n    \"--bjork-text\": \"#171717\",\n    \"--bjork-text-strong\": \"rgba(23, 23, 23, 0.9)\",\n    \"--bjork-text-medium\": \"rgba(23, 23, 23, 0.72)\",\n    \"--bjork-text-muted\": \"rgba(23, 23, 23, 0.56)\",\n    \"--bjork-text-soft\": \"rgba(23, 23, 23, 0.46)\",\n    \"--bjork-text-faint\": \"rgba(23, 23, 23, 0.22)\",\n    \"--bjork-inverted-text\": \"#fff8f0\",\n    \"--bjork-border\": \"#eee6db\",\n    \"--bjork-border-muted\": \"#f5ede2\",\n    \"--bjork-border-strong\": \"#e1d7c8\",\n    \"--bjork-thumb\": \"#a49b8e\",\n    \"--bjork-track\": \"#ded6ca\",\n    \"--bjork-accent\": \"#ec7d43\",\n    \"--bjork-accent-hover\": \"#f0935f\",\n    \"--bjork-accent-soft\": \"rgba(236, 125, 67, 0.1)\",\n    \"--bjork-accent-muted\": \"rgba(236, 125, 67, 0.2)\",\n    \"--bjork-accent-foreground\": \"#3f2112\",\n    \"--bjork-shadow-surface\":\n      \"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    \"--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)\",\n    \"--bjork-shadow-panel\":\n      \"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    \"--bjork-shadow-menu\":\n      \"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    \"--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)\",\n    \"--bjork-ring-offset\": \"#f7f3ea\",\n  },\n  dark: {\n    \"--bjork-bg\": \"#111111\",\n    \"--bjork-surface\": \"#121212\",\n    \"--bjork-surface-muted\": \"rgba(22, 22, 22, 0.92)\",\n    \"--bjork-surface-hover\": \"#181818\",\n    \"--bjork-surface-active\": \"#202020\",\n    \"--bjork-panel\": \"#0d0d0d\",\n    \"--bjork-menu\": \"rgba(18, 18, 18, 0.98)\",\n    \"--bjork-field\": \"rgba(18, 18, 18, 0.95)\",\n    \"--bjork-field-muted\": \"rgba(18, 18, 18, 0.55)\",\n    \"--bjork-field-inset\": \"rgba(9, 9, 9, 0.75)\",\n    \"--bjork-error-bg\": \"#130d0a\",\n    \"--bjork-text\": \"#ededed\",\n    \"--bjork-text-strong\": \"rgba(237, 237, 237, 0.9)\",\n    \"--bjork-text-medium\": \"rgba(237, 237, 237, 0.72)\",\n    \"--bjork-text-muted\": \"rgba(237, 237, 237, 0.6)\",\n    \"--bjork-text-soft\": \"rgba(237, 237, 237, 0.4)\",\n    \"--bjork-text-faint\": \"rgba(237, 237, 237, 0.22)\",\n    \"--bjork-inverted-text\": \"#080808\",\n    \"--bjork-border\": \"#232323\",\n    \"--bjork-border-muted\": \"#1c1c1c\",\n    \"--bjork-border-strong\": \"#343434\",\n    \"--bjork-thumb\": \"#626262\",\n    \"--bjork-track\": \"#090909\",\n    \"--bjork-accent\": \"#ec5c13\",\n    \"--bjork-accent-hover\": \"#f07832\",\n    \"--bjork-accent-soft\": \"rgba(236, 92, 19, 0.12)\",\n    \"--bjork-accent-muted\": \"rgba(236, 92, 19, 0.24)\",\n    \"--bjork-accent-foreground\": \"#fff2ea\",\n    \"--bjork-shadow-surface\":\n      \"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    \"--bjork-shadow-soft\": \"inset 0 1px 0 rgba(255, 255, 255, 0.045)\",\n    \"--bjork-shadow-panel\": \"inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)\",\n    \"--bjork-shadow-menu\":\n      \"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    \"--bjork-shadow-inset\": \"inset 0 1px 10px rgba(0, 0, 0, 0.45)\",\n    \"--bjork-ring-offset\": \"#080808\",\n  },\n};\n\n// Block-only tokens the global sheet does not carry: status inks, a readable accent ink and the scrim.\nconst EXTRAS: Record<BjorkTone, Record<string, string>> = {\n  light: {\n    \"--blk-accent-ink\": \"#b4531f\",\n    \"--blk-accent-fill\": \"#e0692c\",\n    \"--blk-accent-fill-ink\": \"#fffaf5\",\n    \"--blk-success\": \"#1f8a55\",\n    \"--blk-success-soft\": \"rgba(31, 138, 85, 0.1)\",\n    \"--blk-warning\": \"#9a6200\",\n    \"--blk-warning-soft\": \"rgba(168, 107, 0, 0.1)\",\n    \"--blk-error\": \"#c8361f\",\n    \"--blk-error-soft\": \"rgba(200, 54, 31, 0.08)\",\n    \"--blk-scrim\": \"rgba(40, 32, 22, 0.28)\",\n    \"--blk-chart-ghost\": \"rgba(23, 23, 23, 0.28)\",\n  },\n  dark: {\n    \"--blk-accent-ink\": \"#f07832\",\n    \"--blk-accent-fill\": \"#c94f12\",\n    \"--blk-accent-fill-ink\": \"#fff2ea\",\n    \"--blk-success\": \"#4cc38a\",\n    \"--blk-success-soft\": \"rgba(76, 195, 138, 0.12)\",\n    \"--blk-warning\": \"#f2b544\",\n    \"--blk-warning-soft\": \"rgba(242, 181, 68, 0.12)\",\n    \"--blk-error\": \"#ff6a5c\",\n    \"--blk-error-soft\": \"rgba(255, 92, 77, 0.1)\",\n    \"--blk-scrim\": \"rgba(0, 0, 0, 0.55)\",\n    \"--blk-chart-ghost\": \"rgba(237, 237, 237, 0.3)\",\n  },\n};\n\nconst subscribeNoop = () => () => {};\n\n/** True after hydration. Server and first client render agree on false. */\nexport function useHydrated() {\n  return useSyncExternalStore(\n    subscribeNoop,\n    () => true,\n    () => false,\n  );\n}\n\n/** Reduced motion, read only after hydration so the first render matches the server. */\nexport function useBlockReducedMotion() {\n  const hydrated = useHydrated();\n  const reduced = useReducedMotion();\n  return hydrated && reduced === true;\n}\n\n/**\n * Resolves a block's theme to inline custom properties. \"auto\" leaves the library tokens to the page\n * (no flash on load) and only adds the block extras for the resolved tone; \"light\" and \"dark\" pin every token.\n */\nexport function useAppBlockTheme(theme: AppBlockTheme = \"auto\") {\n  const forced = theme === \"auto\" ? undefined : theme;\n  const tone = useBjorkTone(forced);\n  const style = {\n    ...(forced ? TOKENS[forced] : {}),\n    ...EXTRAS[tone],\n    colorScheme: tone,\n  } as CSSProperties;\n  return { tone, style };\n}\n\n/** Width of an element, tracked with ResizeObserver. 0 until measured. */\nexport function useElementWidth(ref: RefObject<HTMLElement | null>) {\n  const [width, setWidth] = useState(0);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    setWidth(el.getBoundingClientRect().width);\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref]);\n  return width;\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 const blockRoot =\n  \"@container relative isolate flex min-h-0 w-full overflow-hidden bg-[var(--bjork-bg)] font-bjork-alpha text-[color:var(--bjork-text)] antialiased selection:bg-[color:var(--bjork-accent-muted)]\";\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"outline\";\ntype ButtonSize = \"sm\" | \"md\" | \"lg\";\n\nconst BUTTON_VARIANTS: Record<ButtonVariant, string> = {\n  primary:\n    \"bg-[var(--blk-accent-fill)] text-[color:var(--blk-accent-fill-ink)] shadow-[inset_0_1px_0_rgba(255,255,255,0.22),0_1px_2px_rgba(0,0,0,0.18)] hover:brightness-[1.08]\",\n  secondary:\n    \"border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)] hover:bg-[var(--bjork-surface-hover)]\",\n  outline:\n    \"border border-[color:var(--bjork-border-strong)] bg-transparent text-[color:var(--bjork-text)] hover:bg-[var(--bjork-surface-hover)]\",\n  ghost:\n    \"bg-transparent text-[color:var(--bjork-text-medium)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]\",\n  danger:\n    \"bg-[var(--blk-error)] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] hover:brightness-[1.08]\",\n};\n\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n  sm: \"h-8 gap-1.5 rounded-[9px] px-2.5 text-[12.5px]\",\n  md: \"h-9 gap-2 rounded-[10px] px-3.5 text-[13px]\",\n  lg: \"h-11 gap-2 rounded-[12px] px-4 text-[14px]\",\n};\n\nexport interface BlockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n}\n\nexport const BlockButton = forwardRef<HTMLButtonElement, BlockButtonProps>(function BlockButton(\n  { variant = \"secondary\", size = \"md\", className, type = \"button\", ...props },\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 whitespace-nowrap font-medium tracking-[-0.01em] transition-[background-color,color,filter,transform,border-color] duration-150 ease-out active:scale-[0.97] disabled:pointer-events-none disabled:opacity-40 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:size-4 [&_svg]:shrink-0\",\n        focusRing,\n        BUTTON_VARIANTS[variant],\n        BUTTON_SIZES[size],\n        className,\n      )}\n      {...props}\n    />\n  );\n});\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Accessible name. Also used as the native tooltip. */\n  label: string;\n  size?: \"sm\" | \"md\";\n  pressed?: boolean;\n}\n\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n  { label, size = \"md\", pressed, className, type = \"button\", children, ...props },\n  ref,\n) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      aria-label={label}\n      title={label}\n      aria-pressed={pressed}\n      className={cn(\n        \"inline-grid shrink-0 cursor-pointer place-items-center rounded-[9px] text-[color:var(--bjork-text-muted)] transition-[background-color,color,transform] duration-150 ease-out hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)] active:scale-[0.94] disabled:pointer-events-none disabled:opacity-40 motion-reduce:transition-none motion-reduce:active:scale-100 aria-pressed:text-[color:var(--blk-accent-ink)] [&_svg]:shrink-0\",\n        size === \"sm\" ? \"size-7 [&_svg]:size-[15px]\" : \"size-9 [&_svg]:size-[17px]\",\n        focusRing,\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n});\n\nexport function Kbd({ children, className }: { children: ReactNode; className?: string }) {\n  return (\n    <kbd\n      className={cn(\n        \"inline-flex h-5 min-w-5 items-center justify-center rounded-[5px] border border-[color:var(--bjork-border-strong)] bg-[var(--bjork-surface)] px-1 font-mono text-[11px] font-medium leading-none text-[color:var(--bjork-text-muted)] shadow-[0_1px_0_var(--bjork-border-strong)]\",\n        className,\n      )}\n    >\n      {children}\n    </kbd>\n  );\n}\n\nexport const inputClass =\n  \"h-10 w-full min-w-0 rounded-[10px] border border-[color:var(--bjork-border)] bg-[var(--bjork-field)] px-3 text-[13.5px] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)] outline-none transition-[border-color,box-shadow,background-color] duration-150 placeholder:text-[color:var(--bjork-text-soft)] hover:border-[color:var(--bjork-border-strong)] focus-visible:border-[color:var(--bjork-accent)] focus-visible:ring-[3px] focus-visible:ring-[color:var(--bjork-accent-soft)] aria-[invalid=true]:border-[color:var(--blk-error)] aria-[invalid=true]:focus-visible:ring-[color:var(--blk-error-soft)] disabled:cursor-not-allowed disabled:opacity-50\";\n\nexport interface BlockFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"children\"> {\n  label: string;\n  hint?: ReactNode;\n  error?: string | null;\n  /** Rendered at the right of the label row, e.g. a \"Forgot?\" link. */\n  labelAside?: ReactNode;\n  /** Rendered inside the field on the right, e.g. a show/hide toggle. */\n  trailing?: ReactNode;\n  className?: string;\n  inputClassName?: string;\n}\n\n/** Label, input, hint and error wired together with ids. The error replaces the hint and is announced. */\nexport const BlockField = forwardRef<HTMLInputElement, BlockFieldProps>(function BlockField(\n  { label, hint, error, labelAside, trailing, className, inputClassName, id, ...props },\n  ref,\n) {\n  const autoId = useId();\n  const inputId = id ?? autoId;\n  const noteId = `${inputId}-note`;\n  const hasNote = Boolean(error) || hint !== undefined;\n  return (\n    <div className={cn(\"flex min-w-0 flex-col gap-1.5\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <label htmlFor={inputId} className=\"text-[12.5px] font-medium text-[color:var(--bjork-text-medium)]\">\n          {label}\n        </label>\n        {labelAside}\n      </div>\n      <div className=\"relative\">\n        <input\n          ref={ref}\n          id={inputId}\n          aria-invalid={error ? true : undefined}\n          aria-describedby={hasNote ? noteId : undefined}\n          className={cn(inputClass, trailing ? \"pr-10\" : undefined, inputClassName)}\n          {...props}\n        />\n        {trailing && <div className=\"absolute inset-y-0 right-1 flex items-center\">{trailing}</div>}\n      </div>\n      {hasNote && (\n        <p\n          id={noteId}\n          role={error ? \"alert\" : undefined}\n          className={cn(\n            \"flex items-start gap-1.5 text-[12px] leading-[1.45]\",\n            error ? \"text-[color:var(--blk-error)]\" : \"text-[color:var(--bjork-text-soft)]\",\n          )}\n        >\n          {error ? (\n            <>\n              <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"mt-[2px] size-3 shrink-0\">\n                <circle cx=\"8\" cy=\"8\" r=\"6.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n                <path d=\"M8 4.75v3.75M8 10.9v.1\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n              </svg>\n              {error}\n            </>\n          ) : (\n            hint\n          )}\n        </p>\n      )}\n    </div>\n  );\n});\n\n/** Initials avatar on a quiet tinted disc. Hue comes from the name so a person keeps their colour. */\nexport function InitialsAvatar({\n  name,\n  size = 28,\n  className,\n}: {\n  name: string;\n  size?: number;\n  className?: string;\n}) {\n  const initials = name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((part) => part[0]?.toUpperCase())\n    .join(\"\");\n  let hash = 0;\n  for (let i = 0; i < name.length; i += 1) hash = (hash * 31 + name.charCodeAt(i)) >>> 0;\n  const hue = [22, 38, 150, 200, 260, 330][hash % 6];\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"inline-grid shrink-0 place-items-center rounded-full font-semibold tracking-[-0.02em] text-[color:var(--bjork-text)]\",\n        className,\n      )}\n      style={{\n        width: size,\n        height: size,\n        fontSize: Math.max(11, Math.round(size * 0.38)),\n        background: `color-mix(in oklab, hsl(${hue} 70% 55%) 22%, var(--bjork-surface-active))`,\n        boxShadow: `inset 0 0 0 1px color-mix(in oklab, hsl(${hue} 70% 55%) 24%, transparent)`,\n      }}\n    >\n      {initials}\n    </span>\n  );\n}\n\nconst FOCUSABLE =\n  'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Keeps focus inside `ref` while `active`, closes on Escape, and hands focus back to whatever had it\n * before. Used by the in-block drawers and dialogs, which are positioned inside the block rather than portalled,\n * so a forced theme reaches them.\n */\nexport function useFocusTrap(ref: RefObject<HTMLElement | null>, active: boolean, onEscape: () => void) {\n  const escapeRef = useRef(onEscape);\n  useLayoutEffect(() => {\n    escapeRef.current = onEscape;\n  });\n  useEffect(() => {\n    if (!active) return;\n    const node = ref.current;\n    if (!node) return;\n    const previous = document.activeElement as HTMLElement | null;\n    const first = node.querySelector<HTMLElement>(\"[data-autofocus]\") ?? node.querySelector<HTMLElement>(FOCUSABLE);\n    (first ?? node).focus({ preventScroll: true });\n    const onKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.stopPropagation();\n        escapeRef.current();\n        return;\n      }\n      if (event.key !== \"Tab\") return;\n      const items = Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n        (el) => el.offsetParent !== null || el === document.activeElement,\n      );\n      if (items.length === 0) return;\n      const head = items[0];\n      const tail = items[items.length - 1];\n      if (event.shiftKey && document.activeElement === head) {\n        event.preventDefault();\n        tail.focus();\n      } else if (!event.shiftKey && document.activeElement === tail) {\n        event.preventDefault();\n        head.focus();\n      }\n    };\n    node.addEventListener(\"keydown\", onKey);\n    return () => {\n      node.removeEventListener(\"keydown\", onKey);\n      if (previous && document.contains(previous)) previous.focus({ preventScroll: true });\n    };\n  }, [active, ref]);\n}\n\nexport interface BlockDrawerProps {\n  open: boolean;\n  onClose: () => void;\n  side?: \"left\" | \"right\";\n  /** Accessible name of the dialog. */\n  label: string;\n  children: ReactNode;\n  className?: string;\n}\n\n/**\n * A modal drawer anchored to the block, not the viewport. Under reduced motion it fades instead of sliding.\n * The block's own content should be marked `inert` while it is open.\n */\nexport function BlockDrawer({ open, onClose, side = \"left\", label, children, className }: BlockDrawerProps) {\n  const panelRef = useRef<HTMLDivElement>(null);\n  const reduce = useBlockReducedMotion();\n  useFocusTrap(panelRef, open, onClose);\n  const hidden = side === \"left\" ? \"-translate-x-full\" : \"translate-x-full\";\n  return (\n    <div className={cn(\"absolute inset-0 z-40\", open ? \"pointer-events-auto\" : \"pointer-events-none\")} aria-hidden={!open}>\n      <div\n        className={cn(\n          \"absolute inset-0 bg-[var(--blk-scrim)] transition-opacity duration-200 ease-out\",\n          open ? \"opacity-100\" : \"opacity-0\",\n        )}\n        onClick={onClose}\n      />\n      <div\n        ref={panelRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label={label}\n        tabIndex={-1}\n        inert={!open}\n        className={cn(\n          \"absolute inset-y-0 flex w-[min(300px,86%)] flex-col bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-menu)] outline-none\",\n          side === \"left\" ? \"left-0 border-r\" : \"right-0 border-l\",\n          \"border-[color:var(--bjork-border)]\",\n          reduce\n            ? cn(\"transition-opacity duration-150\", open ? \"opacity-100\" : \"opacity-0\")\n            : cn(\n                \"transition-transform duration-[280ms] ease-[cubic-bezier(0.32,0.72,0,1)]\",\n                open ? \"translate-x-0\" : hidden,\n              ),\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\n/** Polite status text for screen readers; changes are announced once. */\nexport function useAnnouncer() {\n  const [message, setMessage] = useState(\"\");\n  const announce = useCallback((next: string) => {\n    // Clearing first makes a repeated message announce again.\n    setMessage(\"\");\n    window.requestAnimationFrame(() => setMessage(next));\n  }, []);\n  const region = (\n    <span role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n  return { announce, region };\n}\n\n/** A small toast pinned to the bottom of the block. Purely visual; pair it with `useAnnouncer`. */\nexport function BlockToast({ message, tone = \"neutral\" }: { message: string | null; tone?: \"neutral\" | \"success\" | \"error\" }) {\n  const reduce = useBlockReducedMotion();\n  const dot =\n    tone === \"success\" ? \"var(--blk-success)\" : tone === \"error\" ? \"var(--blk-error)\" : \"var(--bjork-accent)\";\n  return (\n    <div className=\"pointer-events-none absolute inset-x-0 bottom-4 z-50 flex justify-center px-4\" aria-hidden=\"true\">\n      <div\n        className={cn(\n          \"flex max-w-full items-center gap-2.5 rounded-full border border-[color:var(--bjork-border)] bg-[var(--bjork-menu)] py-2 pl-3 pr-4 text-[12.5px] font-medium text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-menu)] backdrop-blur-xl\",\n          reduce ? \"transition-opacity duration-150\" : \"transition-[opacity,transform] duration-200 ease-out\",\n          message ? \"translate-y-0 opacity-100\" : \"translate-y-2 opacity-0\",\n        )}\n      >\n        <span className=\"size-1.5 shrink-0 rounded-full\" style={{ background: dot }} />\n        <span className=\"truncate\">{message}</span>\n      </div>\n    </div>\n  );\n}\n\n/** Shows a toast for `ms` and announces it. */\nexport function useBlockToast(ms = 2600) {\n  const { announce, region } = useAnnouncer();\n  const [toast, setToast] = useState<{ message: string; tone: \"neutral\" | \"success\" | \"error\" } | null>(null);\n  const timer = useRef<number | undefined>(undefined);\n  const show = useCallback(\n    (message: string, tone: \"neutral\" | \"success\" | \"error\" = \"neutral\") => {\n      window.clearTimeout(timer.current);\n      setToast({ message, tone });\n      announce(message);\n      timer.current = window.setTimeout(() => setToast(null), ms);\n    },\n    [announce, ms],\n  );\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n  const node = (\n    <>\n      <BlockToast message={toast?.message ?? null} tone={toast?.tone} />\n      {region}\n    </>\n  );\n  return { show, node };\n}\n\nexport interface BlockDialogProps {\n  open: boolean;\n  onClose: () => void;\n  /** Visible title; also the dialog's accessible name. */\n  title: string;\n  description?: ReactNode;\n  children?: ReactNode;\n  /** Buttons for the footer, right-aligned. */\n  footer?: ReactNode;\n  tone?: \"neutral\" | \"danger\";\n  className?: string;\n}\n\n/**\n * A modal dialog positioned inside the block (not portalled), so a forced theme reaches it. Traps focus, closes\n * on Escape or a scrim click, and returns focus to the trigger. Mark the block's own content `inert` while open.\n */\nexport function BlockDialog({ open, onClose, title, description, children, footer, tone = \"neutral\", className }: BlockDialogProps) {\n  const panelRef = useRef<HTMLDivElement>(null);\n  const reduce = useBlockReducedMotion();\n  const titleId = useId();\n  const descId = useId();\n  useFocusTrap(panelRef, open, onClose);\n  return (\n    <div\n      className={cn(\"absolute inset-0 z-50 grid place-items-center p-4\", open ? \"pointer-events-auto\" : \"pointer-events-none\")}\n      aria-hidden={!open}\n    >\n      <div\n        className={cn(\n          \"absolute inset-0 bg-[var(--blk-scrim)] transition-opacity duration-200 ease-out\",\n          open ? \"opacity-100\" : \"opacity-0\",\n        )}\n        onClick={onClose}\n      />\n      <div\n        ref={panelRef}\n        role={tone === \"danger\" ? \"alertdialog\" : \"dialog\"}\n        aria-modal=\"true\"\n        aria-labelledby={titleId}\n        aria-describedby={description ? descId : undefined}\n        tabIndex={-1}\n        inert={!open}\n        className={cn(\n          \"relative flex max-h-full w-full max-w-[440px] flex-col overflow-hidden rounded-[18px] border border-[color:var(--bjork-border)] bg-[var(--bjork-surface)] shadow-[var(--bjork-shadow-menu)] outline-none\",\n          reduce\n            ? cn(\"transition-opacity duration-150\", open ? \"opacity-100\" : \"opacity-0\")\n            : cn(\n                \"transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]\",\n                open ? \"scale-100 opacity-100\" : \"scale-[0.97] opacity-0\",\n              ),\n          className,\n        )}\n      >\n        <div className=\"overflow-y-auto px-5 pb-4 pt-5\">\n          <h2 id={titleId} className=\"text-[16px] font-semibold tracking-[-0.015em]\">\n            {title}\n          </h2>\n          {description && (\n            <div id={descId} className=\"mt-1.5 text-[13px] leading-[1.55] text-[color:var(--bjork-text-muted)]\">\n              {description}\n            </div>\n          )}\n          {children && <div className=\"mt-4\">{children}</div>}\n        </div>\n        {footer && (\n          <div className=\"flex flex-wrap items-center justify-end gap-2 border-t border-[color:var(--bjork-border)] bg-[var(--bjork-panel)] px-5 py-3\">\n            {footer}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport interface BlockSwitchProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  /** Accessible name when there is no visible label wired with aria-labelledby. */\n  label?: string;\n}\n\n/** A role=\"switch\" toggle. Space and Enter flip it; it reads as on/off to assistive tech. */\nexport const BlockSwitch = forwardRef<HTMLButtonElement, BlockSwitchProps>(function BlockSwitch(\n  { checked, onCheckedChange, label, className, disabled, ...props },\n  ref,\n) {\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={checked}\n      aria-label={label}\n      disabled={disabled}\n      onClick={() => onCheckedChange(!checked)}\n      className={cn(\n        \"relative inline-flex h-[22px] w-[38px] shrink-0 cursor-pointer items-center rounded-full border transition-[background-color,border-color] duration-200 ease-out disabled:cursor-not-allowed disabled:opacity-45 motion-reduce:transition-none\",\n        checked\n          ? \"border-transparent bg-[var(--blk-accent-fill)]\"\n          : \"border-[color:var(--bjork-border-strong)] bg-[var(--bjork-field-inset)] shadow-[var(--bjork-shadow-inset)]\",\n        focusRing,\n        className,\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute left-[2px] size-4 rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.3)] transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n          checked ? \"translate-x-4\" : \"translate-x-0\",\n        )}\n      />\n    </button>\n  );\n});\n\nexport function BlockSpinner({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className={cn(\"animate-spin motion-reduce:animate-[spin_1.6s_linear_infinite]\", className)}>\n      <circle cx=\"8\" cy=\"8\" r=\"6\" fill=\"none\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"1.8\" />\n      <path d=\"M14 8a6 6 0 0 0-6-6\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/blocks/app-block-kit.tsx"
    },
    {
      "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/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": "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": "Blocks",
    "registryUrl": "https://ui.isaiahbjork.com/app-dashboard-shell.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/app-dashboard-shell.json"
  },
  "type": "registry:block"
}