{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "changelog-table",
  "title": "Changelog table",
  "description": "Changelog table is a production-ready tables component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/tables/changelog-table.tsx",
      "content": "\"use client\";\n\nimport {\n  Fragment,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useTheme } from \"next-themes\";\nimport {\n  AlertTriangle,\n  ArrowUpRight,\n  ChevronRight,\n  Search,\n  ShieldCheck,\n  Sparkles,\n  TrendingUp,\n  Wrench,\n  X,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useElementSize } from \"../_core/canvas\";\nimport {\n  type BjorkTableThemeMode,\n  getBjorkSignalPalette,\n  getBjorkTablePalette,\n  useBjorkTableIsDark,\n} from \"./table-theme\";\n\nexport type ChangeType = \"feature\" | \"fix\" | \"improvement\" | \"breaking\" | \"security\";\n\nexport interface ChangelogChange {\n  type: ChangeType;\n  text: string;\n}\n\nexport interface ChangelogRelease {\n  /** Semver without the leading \"v\", e.g. \"3.2.0\". */\n  version: string;\n  /** ISO date, e.g. \"2026-09-30\". */\n  date: string;\n  summary: string;\n  changes: ChangelogChange[];\n  /** Tags shown in the row. Defaults to the distinct types in `changes`. */\n  types?: ChangeType[];\n  href?: string;\n}\n\nexport interface ChangelogTableProps {\n  releases?: ChangelogRelease[];\n  /** Adds a header row per minor version (3.2, 3.1, ...). */\n  groupByMinor?: boolean;\n  /** Versions expanded on first render. */\n  defaultExpanded?: string[];\n  onReleaseToggle?: (version: string, expanded: boolean) => void;\n  productName?: string;\n  maxHeight?: number;\n  loading?: boolean;\n  className?: string;\n  theme?: BjorkTableThemeMode;\n  enableAnimations?: boolean;\n}\n\nconst TYPE_ORDER: ChangeType[] = [\"feature\", \"improvement\", \"fix\", \"breaking\", \"security\"];\n\nconst TYPE_META: Record<ChangeType, { label: string; icon: LucideIcon; tone: \"green\" | \"red\" | \"orange\" | \"neutral\" }> = {\n  feature: { label: \"Feature\", icon: Sparkles, tone: \"green\" },\n  improvement: { label: \"Improvement\", icon: TrendingUp, tone: \"neutral\" },\n  fix: { label: \"Fix\", icon: Wrench, tone: \"neutral\" },\n  breaking: { label: \"Breaking\", icon: AlertTriangle, tone: \"red\" },\n  security: { label: \"Security\", icon: ShieldCheck, tone: \"orange\" },\n};\n\nexport const CHANGELOG_TABLE_SAMPLE: ChangelogRelease[] = [\n  {\n    version: \"3.2.0\",\n    date: \"2026-09-30\",\n    summary: \"Branch previews for sync rules and a faster cold start\",\n    href: \"#v3.2.0\",\n    changes: [\n      { type: \"feature\", text: \"`driftline preview` spins up an isolated replica for any branch of your sync rules.\" },\n      { type: \"feature\", text: \"Rules can reference `auth.claims` directly, no custom resolver needed.\" },\n      { type: \"improvement\", text: \"Cold start on mobile clients is 38% faster thanks to a lazily hydrated index.\" },\n      { type: \"improvement\", text: \"`driftline push --dry-run` now prints the full diff plan as a table.\" },\n    ],\n  },\n  {\n    version: \"3.1.2\",\n    date: \"2026-09-18\",\n    summary: \"Patches a token replay window in the websocket handshake\",\n    href: \"#v3.1.2\",\n    changes: [\n      { type: \"security\", text: \"Handshake nonces now expire after 30 seconds, closing a narrow replay window.\" },\n      { type: \"fix\", text: \"Reconnects no longer drop queued mutations when the socket closes mid-flush.\" },\n    ],\n  },\n  {\n    version: \"3.1.1\",\n    date: \"2026-09-09\",\n    summary: \"Fixes WAL checkpoint stalls on large batches\",\n    href: \"#v3.1.1\",\n    changes: [\n      { type: \"fix\", text: \"Batches over 10k rows no longer stall the WAL checkpoint on SQLite targets.\" },\n      { type: \"fix\", text: \"`driftline status` reports the correct lag when the replica clock drifts.\" },\n      { type: \"fix\", text: \"Windows paths with spaces are quoted in generated migration scripts.\" },\n    ],\n  },\n  {\n    version: \"3.1.0\",\n    date: \"2026-08-27\",\n    summary: \"Conflict inspector and typed client generation\",\n    href: \"#v3.1.0\",\n    changes: [\n      { type: \"feature\", text: \"New conflict inspector shows both sides of a merge with field-level blame.\" },\n      { type: \"feature\", text: \"`driftline codegen --lang ts` emits typed clients from your schema.\" },\n      { type: \"improvement\", text: \"Schema diffs ignore column order, so reordering no longer triggers a migration.\" },\n      { type: \"fix\", text: \"Composite primary keys round-trip correctly through the REST bridge.\" },\n    ],\n  },\n  {\n    version: \"3.0.1\",\n    date: \"2026-08-12\",\n    summary: \"Hotfix for the 3.0 config migrator\",\n    href: \"#v3.0.1\",\n    changes: [\n      { type: \"fix\", text: \"The config migrator keeps comments and trailing commas in `driftline.toml`.\" },\n    ],\n  },\n  {\n    version: \"3.0.0\",\n    date: \"2026-08-04\",\n    summary: \"New rules engine, Node 18 support dropped\",\n    href: \"#v3.0.0\",\n    changes: [\n      { type: \"breaking\", text: \"Node 18 is no longer supported. The CLI and SDK require Node 20.11 or later.\" },\n      { type: \"breaking\", text: \"`sync.rules.js` is replaced by declarative `rules.toml`. Run `driftline migrate-rules`.\" },\n      { type: \"feature\", text: \"The new rules engine evaluates filters on the server, cutting payloads by up to 70%.\" },\n      { type: \"improvement\", text: \"Error messages link to the exact rule and line that failed.\" },\n    ],\n  },\n  {\n    version: \"2.9.4\",\n    date: \"2026-07-21\",\n    summary: \"Updates the crypto binding and tightens CORS defaults\",\n    href: \"#v2.9.4\",\n    changes: [\n      { type: \"security\", text: \"Bumps the bundled crypto binding to patch a timing side channel in key comparison.\" },\n      { type: \"security\", text: \"The dev server no longer allows wildcard CORS origins by default.\" },\n      { type: \"fix\", text: \"Telemetry opt-out is respected when set through an environment variable.\" },\n    ],\n  },\n  {\n    version: \"2.9.3\",\n    date: \"2026-07-08\",\n    summary: \"Smaller bundles for browser clients\",\n    href: \"#v2.9.3\",\n    changes: [\n      { type: \"improvement\", text: \"The browser SDK drops its polyfills and is 11 kB smaller gzipped.\" },\n      { type: \"improvement\", text: \"Log lines include the replica id for easier tracing across regions.\" },\n    ],\n  },\n  {\n    version: \"2.9.0\",\n    date: \"2026-06-24\",\n    summary: \"Offline queues survive app restarts\",\n    href: \"#v2.9.0\",\n    changes: [\n      { type: \"feature\", text: \"Pending mutations persist to disk and replay in order after a restart.\" },\n      { type: \"feature\", text: \"New `onQueueChange` hook reports queue depth for offline indicators.\" },\n    ],\n  },\n];\n\nconst subscribeNoop = () => () => {};\n\n// False during SSR and hydration, true afterwards.\nfunction useMounted() {\n  return useSyncExternalStore(subscribeNoop, () => true, () => false);\n}\n\nfunction parseSemver(version: string) {\n  const [major = 0, minor = 0, patch = 0] = version\n    .replace(/^v/, \"\")\n    .split(/[.-]/)\n    .map((part) => Number.parseInt(part, 10) || 0);\n  return { major, minor, patch };\n}\n\nfunction compareSemverDesc(a: string, b: string) {\n  const x = parseSemver(a);\n  const y = parseSemver(b);\n  return y.major - x.major || y.minor - x.minor || y.patch - x.patch;\n}\n\nconst dateFormat = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  day: \"numeric\",\n  year: \"numeric\",\n  timeZone: \"UTC\",\n});\n\nfunction formatDate(iso: string) {\n  const date = new Date(`${iso}T00:00:00Z`);\n  return Number.isNaN(date.getTime()) ? iso : dateFormat.format(date);\n}\n\nfunction releaseTypes(release: ChangelogRelease) {\n  const set = new Set<ChangeType>(release.types ?? release.changes.map((change) => change.type));\n  return TYPE_ORDER.filter((type) => set.has(type));\n}\n\nexport function ChangelogTable({\n  releases = CHANGELOG_TABLE_SAMPLE,\n  groupByMinor = false,\n  defaultExpanded = [],\n  onReleaseToggle,\n  productName = \"Driftline\",\n  maxHeight = 560,\n  loading = false,\n  className,\n  theme = \"auto\",\n  enableAnimations = true,\n}: ChangelogTableProps) {\n  const mounted = useMounted();\n  const { resolvedTheme } = useTheme();\n  const forcedTheme = theme === \"auto\" ? undefined : theme;\n  const detectedIsDark = useBjorkTableIsDark(resolvedTheme, forcedTheme);\n  const isDark = !mounted && theme === \"auto\" ? true : detectedIsDark;\n  const palette = getBjorkTablePalette(isDark);\n  const shouldReduceMotion = useReducedMotion();\n  const shouldAnimate = enableAnimations && !shouldReduceMotion;\n\n  const [query, setQuery] = useState(\"\");\n  const [activeTypes, setActiveTypes] = useState<Set<ChangeType>>(() => new Set());\n  const [expanded, setExpanded] = useState<Set<string>>(() => new Set(defaultExpanded));\n  const [instant, setInstant] = useState(false);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const { width: rootWidth } = useElementSize(rootRef);\n  const wide = !(rootWidth > 0 && rootWidth < 600);\n  // Below ~840px the type tags move under the summary so it stays readable.\n  const showTypeCol = wide && !(rootWidth > 0 && rootWidth < 840);\n  const uid = useId();\n\n  const sorted = useMemo(\n    () => [...releases].sort((a, b) => compareSemverDesc(a.version, b.version)),\n    [releases]\n  );\n  const latestVersion = sorted[0]?.version;\n\n  const typeCounts = useMemo(() => {\n    const counts = new Map<ChangeType, number>();\n    for (const release of sorted) {\n      for (const type of releaseTypes(release)) counts.set(type, (counts.get(type) ?? 0) + 1);\n    }\n    return counts;\n  }, [sorted]);\n\n  const needle = query.trim().toLowerCase();\n  const visible = useMemo(\n    () =>\n      sorted.filter((release) => {\n        if (activeTypes.size > 0 && !releaseTypes(release).some((type) => activeTypes.has(type))) {\n          return false;\n        }\n        if (!needle) return true;\n        return (\n          release.version.toLowerCase().includes(needle.replace(/^v/, \"\")) ||\n          release.summary.toLowerCase().includes(needle) ||\n          release.changes.some((change) => change.text.toLowerCase().includes(needle))\n        );\n      }),\n    [sorted, activeTypes, needle]\n  );\n\n  const groups = useMemo(() => {\n    if (!groupByMinor) return [{ key: \"all\", label: \"\", releases: visible }];\n    const map = new Map<string, ChangelogRelease[]>();\n    for (const release of visible) {\n      const { major, minor } = parseSemver(release.version);\n      const key = `${major}.${minor}`;\n      map.set(key, [...(map.get(key) ?? []), release]);\n    }\n    return [...map.entries()].map(([key, list]) => ({ key, label: `v${key}`, releases: list }));\n  }, [visible, groupByMinor]);\n\n  const toggle = (version: string, fromKeyboard: boolean) => {\n    setInstant(fromKeyboard);\n    setExpanded((prev) => {\n      const next = new Set(prev);\n      const open = !next.has(version);\n      if (open) next.add(version);\n      else next.delete(version);\n      onReleaseToggle?.(version, open);\n      return next;\n    });\n  };\n\n  const toggleType = (type: ChangeType) => {\n    setActiveTypes((prev) => {\n      const next = new Set(prev);\n      if (next.has(type)) next.delete(type);\n      else next.add(type);\n      return next;\n    });\n  };\n\n  const clearFilters = () => {\n    setQuery(\"\");\n    setActiveTypes(new Set());\n  };\n\n  const onToggleKey = (event: KeyboardEvent<HTMLButtonElement>, version: string) => {\n    const order = visible.map((release) => release.version);\n    const index = order.indexOf(version);\n    let next = -1;\n    if (event.key === \"ArrowDown\") next = Math.min(order.length - 1, index + 1);\n    if (event.key === \"ArrowUp\") next = Math.max(0, index - 1);\n    if (event.key === \"Home\") next = 0;\n    if (event.key === \"End\") next = order.length - 1;\n    if (event.key === \"ArrowRight\" && !expanded.has(version)) {\n      event.preventDefault();\n      toggle(version, true);\n      return;\n    }\n    if (event.key === \"ArrowLeft\" && expanded.has(version)) {\n      event.preventDefault();\n      toggle(version, true);\n      return;\n    }\n    if (next < 0) return;\n    event.preventDefault();\n    event.currentTarget\n      .closest(\"table\")\n      ?.querySelector<HTMLButtonElement>(`[data-release-toggle=\"${order[next]}\"]`)\n      ?.focus();\n  };\n\n  const highlight = (text: string): ReactNode => {\n    if (!needle) return text;\n    const lower = text.toLowerCase();\n    const parts: ReactNode[] = [];\n    let cursor = 0;\n    let hit = lower.indexOf(needle);\n    while (hit !== -1) {\n      if (hit > cursor) parts.push(text.slice(cursor, hit));\n      parts.push(\n        <mark\n          key={hit}\n          className={cn(\"rounded-[3px] px-[1px]\", isDark ? \"bg-[#ec5c13]/24 text-[#ededed]\" : \"bg-[#ec5c13]/18 text-[#171717]\")}\n        >\n          {text.slice(hit, hit + needle.length)}\n        </mark>\n      );\n      cursor = hit + needle.length;\n      hit = lower.indexOf(needle, cursor);\n    }\n    if (cursor < text.length) parts.push(text.slice(cursor));\n    return parts;\n  };\n\n  // Renders `code` spans inside change notes.\n  const renderChangeText = (text: string) =>\n    text.split(/(`[^`]+`)/g).map((part, index) =>\n      part.startsWith(\"`\") && part.endsWith(\"`\") ? (\n        <code\n          key={index}\n          className={cn(\n            \"rounded-[5px] border px-1 py-[1px] font-mono text-[11.5px]\",\n            isDark ? \"border-[#2a2a2a] bg-[#1a1a1a] text-[#ededed]/82\" : \"border-[#eadfce] bg-[#f8f2e7] text-[#171717]/80\"\n          )}\n        >\n          {highlight(part.slice(1, -1))}\n        </code>\n      ) : (\n        <Fragment key={index}>{highlight(part)}</Fragment>\n      )\n    );\n\n  const strongText = isDark ? \"text-[#ededed]\" : \"text-[#171717]\";\n  const headerSurface = isDark ? \"bg-[#181818]\" : \"bg-[#fbf7ef]\";\n  const focusRing = \"outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/50\";\n  const headerLabel = \"text-[11px] font-medium uppercase tracking-[0.08em]\";\n  const columnCount = wide ? (showTypeCol ? 5 : 4) : 3;\n  const animateRows = shouldAnimate && !instant;\n\n  const renderTag = (type: ChangeType) => {\n    const meta = TYPE_META[type];\n    const tone = getBjorkSignalPalette(meta.tone, isDark);\n    const Icon = meta.icon;\n    return (\n      <span\n        key={type}\n        className={cn(\n          \"inline-flex items-center gap-1 rounded-[6px] border px-1.5 py-[2px] text-[11px] font-medium leading-[14px]\",\n          tone.bgColor,\n          tone.borderColor,\n          tone.textColor\n        )}\n      >\n        <Icon className=\"size-3\" aria-hidden=\"true\" strokeWidth={2.2} />\n        {meta.label}\n      </span>\n    );\n  };\n\n  const filtersActive = needle.length > 0 || activeTypes.size > 0;\n\n  return (\n    <div ref={rootRef} className={cn(\"mx-auto w-full max-w-[1040px]\", className)}>\n      <div className={cn(\"relative overflow-hidden rounded-[18px] border\", palette.container)}>\n        {/* Toolbar */}\n        <div className={cn(\"flex flex-col gap-3 border-b px-4 py-3\", palette.divider)}>\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div className=\"min-w-0\">\n              <p className={cn(\"text-[13px] font-medium\", palette.primaryText)}>{productName} releases</p>\n              <p className={cn(\"text-[12px] tabular-nums\", palette.secondaryText)}>\n                {filtersActive ? `${visible.length} of ${sorted.length} releases` : `${sorted.length} releases`}\n              </p>\n            </div>\n            <label\n              className={cn(\n                \"relative flex h-8 min-w-0 items-center rounded-[9px] border transition-shadow focus-within:ring-2 focus-within:ring-[#ec5c13]/40\",\n                wide ? \"w-[240px]\" : \"w-full\",\n                palette.control\n              )}\n            >\n              <span className=\"sr-only\">Search releases</span>\n              <Search className=\"pointer-events-none ml-2.5 size-3.5 shrink-0 opacity-60\" aria-hidden=\"true\" />\n              <input\n                type=\"search\"\n                value={query}\n                onChange={(event) => setQuery(event.target.value)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Escape\" && query) {\n                    event.preventDefault();\n                    setQuery(\"\");\n                  }\n                }}\n                placeholder=\"Search versions or notes\"\n                className={cn(\n                  \"h-full min-w-0 flex-1 bg-transparent px-2 text-[12.5px] outline-none [&::-webkit-search-cancel-button]:hidden\",\n                  isDark ? \"placeholder:text-[#ededed]/30\" : \"placeholder:text-[#171717]/36\",\n                  strongText\n                )}\n              />\n              {query && (\n                <button\n                  type=\"button\"\n                  onClick={() => setQuery(\"\")}\n                  aria-label=\"Clear search\"\n                  className={cn(\"mr-1 inline-flex size-6 items-center justify-center rounded-[7px]\", focusRing, palette.menuItem)}\n                >\n                  <X className=\"size-3.5\" aria-hidden=\"true\" />\n                </button>\n              )}\n            </label>\n          </div>\n          <div role=\"group\" aria-label=\"Filter by change type\" className=\"-mx-1 flex flex-wrap gap-1.5 px-1\">\n            {TYPE_ORDER.filter((type) => typeCounts.has(type)).map((type) => {\n              const pressed = activeTypes.has(type);\n              const meta = TYPE_META[type];\n              const tone = getBjorkSignalPalette(meta.tone, isDark);\n              const Icon = meta.icon;\n              return (\n                <button\n                  key={type}\n                  type=\"button\"\n                  aria-pressed={pressed}\n                  onClick={() => toggleType(type)}\n                  className={cn(\n                    \"inline-flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-[12px] font-medium transition-colors\",\n                    focusRing,\n                    pressed\n                      ? cn(tone.bgColor, tone.borderColor, tone.textColor)\n                      : cn(palette.control)\n                  )}\n                >\n                  <Icon className={cn(\"size-3\", pressed ? \"\" : \"opacity-60\")} aria-hidden=\"true\" />\n                  {meta.label}\n                  <span className={cn(\"tabular-nums\", pressed ? \"opacity-80\" : \"opacity-50\")}>{typeCounts.get(type)}</span>\n                </button>\n              );\n            })}\n            {activeTypes.size > 0 && (\n              <button\n                type=\"button\"\n                onClick={() => setActiveTypes(new Set())}\n                className={cn(\"inline-flex h-7 items-center rounded-full px-2 text-[12px] underline-offset-2 hover:underline\", focusRing, palette.secondaryText)}\n              >\n                Clear\n              </button>\n            )}\n          </div>\n        </div>\n\n        <div\n          role=\"region\"\n          aria-label={`${productName} release history, scrollable`}\n          tabIndex={0}\n          className={cn(\"relative overflow-auto overscroll-contain\", focusRing, \"focus-visible:ring-inset\")}\n          style={{ maxHeight }}\n        >\n          <table className=\"w-full table-fixed border-separate border-spacing-0\">\n            <caption className=\"sr-only\">\n              {productName} release history. Expand a release to read every change.\n            </caption>\n            <colgroup>\n              <col style={{ width: wide ? 140 : 124 }} />\n              {wide && <col style={{ width: 118 }} />}\n              {showTypeCol && <col style={{ width: 230 }} />}\n              <col />\n              <col style={{ width: 48 }} />\n            </colgroup>\n            <thead>\n              <tr>\n                {[\n                  { label: \"Version\", show: true },\n                  { label: \"Date\", show: wide },\n                  { label: \"Type\", show: showTypeCol },\n                  { label: \"Summary\", show: true },\n                  { label: \"Link\", show: true, sr: true },\n                ]\n                  .filter((column) => column.show)\n                  .map((column) => (\n                    <th\n                      key={column.label}\n                      scope=\"col\"\n                      className={cn(\n                        \"sticky top-0 z-20 border-b px-3 py-2.5 text-left\",\n                        headerSurface,\n                        palette.divider,\n                        palette.header.split(\" \").filter((c) => c.startsWith(\"text-\")),\n                        headerLabel,\n                        column.label === \"Version\" && \"pl-4\"\n                      )}\n                    >\n                      {column.sr ? <span className=\"sr-only\">{column.label}</span> : column.label}\n                    </th>\n                  ))}\n              </tr>\n            </thead>\n            {loading ? (\n              <tbody aria-busy=\"true\">\n                {Array.from({ length: 6 }, (_, row) => (\n                  <tr key={row}>\n                    {Array.from({ length: columnCount }, (_, cell) => (\n                      <td key={cell} className={cn(\"border-b px-3 py-3.5\", palette.divider, cell === 0 && \"pl-4\")}>\n                        <span\n                          className={cn(\"block h-2.5 animate-pulse rounded-full motion-reduce:animate-none\", palette.mutedSurface)}\n                          style={{ width: cell === columnCount - 1 ? 16 : `${44 + ((row * 13 + cell * 29) % 46)}%` }}\n                        />\n                      </td>\n                    ))}\n                  </tr>\n                ))}\n              </tbody>\n            ) : (\n              groups.map((group, groupIndex) => (\n                <tbody key={group.key}>\n                  {groupByMinor && (\n                    <tr>\n                      <th\n                        scope=\"colgroup\"\n                        colSpan={columnCount}\n                        className={cn(\"border-b px-4 py-1.5 text-left\", palette.divider, palette.mutedSurface)}\n                      >\n                        <span className={cn(\"font-mono text-[11.5px] font-medium\", palette.primaryText)}>{group.label}</span>\n                        <span className={cn(\"ml-2 text-[11px] tabular-nums\", palette.secondaryText)}>\n                          {group.releases.length} {group.releases.length === 1 ? \"release\" : \"releases\"}\n                        </span>\n                      </th>\n                    </tr>\n                  )}\n                  {group.releases.map((release, index) => {\n                    const open = expanded.has(release.version);\n                    const types = releaseTypes(release);\n                    const isLatest = release.version === latestVersion;\n                    const detailsId = `${uid}-details-${release.version}`;\n                    const order = groupIndex * 100 + index;\n                    return (\n                      <Fragment key={release.version}>\n                        <motion.tr\n                          initial={shouldAnimate ? { opacity: 0, y: 6 } : false}\n                          animate={{ opacity: 1, y: 0 }}\n                          transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1], delay: Math.min(order, 10) * 0.035 }}\n                          onClick={(event) => {\n                            if ((event.target as HTMLElement).closest(\"a,button\")) return;\n                            toggle(release.version, false);\n                          }}\n                          className={cn(\n                            \"group/row cursor-pointer transition-colors\",\n                            open\n                              ? isDark\n                                ? \"bg-[#161616]\"\n                                : \"bg-[#faf5ec]\"\n                              : isDark\n                                ? \"hover:bg-[#181818]/70\"\n                                : \"hover:bg-[#f8f2e7]/72\"\n                          )}\n                        >\n                          <th scope=\"row\" className={cn(\"border-b py-0 pl-2 pr-3 text-left font-normal\", open ? \"border-transparent\" : palette.divider)}>\n                            <button\n                              data-release-toggle={release.version}\n                              type=\"button\"\n                              aria-expanded={open}\n                              aria-controls={detailsId}\n                              onClick={(event) => toggle(release.version, event.detail === 0)}\n                              onKeyDown={(event) => onToggleKey(event, release.version)}\n                              className={cn(\"flex w-full min-w-0 items-center gap-1.5 rounded-[8px] px-1.5 py-3 text-left\", focusRing)}\n                            >\n                              <ChevronRight\n                                aria-hidden=\"true\"\n                                className={cn(\n                                  \"size-3.5 shrink-0 transition-transform duration-200 motion-reduce:transition-none\",\n                                  palette.secondaryText,\n                                  open && \"rotate-90\"\n                                )}\n                              />\n                              <span className=\"min-w-0\">\n                                <span className={cn(\"block font-mono text-[12.5px] font-medium tabular-nums\", strongText)}>\n                                  {highlight(`v${release.version}`)}\n                                </span>\n                                {!wide && (\n                                  <time dateTime={release.date} className={cn(\"block whitespace-nowrap text-[11px] tabular-nums\", palette.secondaryText)}>\n                                    {formatDate(release.date)}\n                                  </time>\n                                )}\n                              </span>\n                              <span className=\"sr-only\">, {open ? \"hide\" : \"show\"} {release.changes.length} changes</span>\n                            </button>\n                          </th>\n                          {wide && (\n                            <td className={cn(\"border-b px-3 py-3\", open ? \"border-transparent\" : palette.divider)}>\n                              <time dateTime={release.date} className={cn(\"text-[12.5px] tabular-nums\", palette.secondaryText)}>\n                                {formatDate(release.date)}\n                              </time>\n                            </td>\n                          )}\n                          {showTypeCol && (\n                            <td className={cn(\"border-b px-3 py-2.5\", open ? \"border-transparent\" : palette.divider)}>\n                              <span className=\"flex flex-wrap gap-1\">\n                                {types.slice(0, 2).map((type) => (\n                                  renderTag(type)\n                                ))}\n                                {types.length > 2 && (\n                                  <span\n                                    className={cn(\"inline-flex items-center rounded-[6px] px-1 text-[11px] tabular-nums\", palette.secondaryText)}\n                                    title={types.slice(2).map((type) => TYPE_META[type].label).join(\", \")}\n                                  >\n                                    +{types.length - 2}\n                                    <span className=\"sr-only\"> more: {types.slice(2).map((type) => TYPE_META[type].label).join(\", \")}</span>\n                                  </span>\n                                )}\n                              </span>\n                            </td>\n                          )}\n                          <td className={cn(\"border-b px-3 py-3\", open ? \"border-transparent\" : palette.divider)}>\n                            <span className=\"flex min-w-0 items-center gap-2\">\n                              <span className={cn(\"min-w-0 text-[13px] leading-[1.4]\", showTypeCol && \"truncate\", palette.primaryText)}>\n                                {highlight(release.summary)}\n                              </span>\n                              {isLatest && (\n                                <span className={cn(\"shrink-0 rounded-[6px] border px-1.5 py-[1px] text-[11px] font-medium\", palette.accentBg, palette.accentBorder, palette.accent)}>\n                                  Latest\n                                </span>\n                              )}\n                            </span>\n                            {!showTypeCol && (\n                              <span className=\"mt-1.5 flex flex-wrap gap-1\">\n                                {types.map((type) => (\n                                  renderTag(type)\n                                ))}\n                              </span>\n                            )}\n                          </td>\n                          <td className={cn(\"border-b px-2 py-3 text-right\", open ? \"border-transparent\" : palette.divider)}>\n                            {release.href && (\n                              <a\n                                href={release.href}\n                                aria-label={`Release notes for v${release.version}`}\n                                className={cn(\n                                  \"inline-flex size-7 items-center justify-center rounded-[8px] transition-colors\",\n                                  focusRing,\n                                  palette.secondaryText,\n                                  isDark ? \"hover:bg-[#232323] hover:text-[#ededed]\" : \"hover:bg-[#f1e8dc] hover:text-[#171717]\"\n                                )}\n                              >\n                                <ArrowUpRight className=\"size-3.5\" aria-hidden=\"true\" />\n                              </a>\n                            )}\n                          </td>\n                        </motion.tr>\n                        <tr className={open ? (isDark ? \"bg-[#161616]\" : \"bg-[#faf5ec]\") : undefined}>\n                          <td\n                            id={detailsId}\n                            colSpan={columnCount}\n                            className={cn(\"p-0\", open && cn(\"border-b\", palette.divider))}\n                          >\n                            <AnimatePresence initial={false}>\n                              {open && (\n                                <motion.div\n                                  key=\"details\"\n                                  initial={animateRows ? { height: 0, opacity: 0 } : false}\n                                  animate={{ height: \"auto\", opacity: 1 }}\n                                  exit={animateRows ? { height: 0, opacity: 0 } : { height: 0, opacity: 0, transition: { duration: 0 } }}\n                                  transition={{ height: { type: \"spring\", stiffness: 420, damping: 40 }, opacity: { duration: 0.18 } }}\n                                  className=\"overflow-hidden\"\n                                >\n                                  <ul\n                                    aria-label={`Changes in v${release.version}`}\n                                    className={cn(\"flex flex-col gap-2 pb-4 pr-4 pt-0.5\", wide ? \"pl-[44px]\" : \"pl-[30px]\")}\n                                  >\n                                    {release.changes.map((change, changeIndex) => {\n                                      const meta = TYPE_META[change.type];\n                                      const tone = getBjorkSignalPalette(meta.tone, isDark);\n                                      return (\n                                        <li key={changeIndex} className=\"flex items-start gap-2.5 text-[13px] leading-[1.5]\">\n                                          <span aria-hidden=\"true\" className={cn(\"mt-[8px] size-1.5 shrink-0 rounded-full\", tone.dotColor)} />\n                                          <span className={cn(\"w-[84px] shrink-0 text-[11.5px] font-medium leading-[19.5px]\", tone.textColor, !wide && \"hidden\")}>\n                                            {meta.label}\n                                          </span>\n                                          <span className={palette.primaryText}>\n                                            {!wide && <span className=\"sr-only\">{meta.label}: </span>}\n                                            {renderChangeText(change.text)}\n                                          </span>\n                                        </li>\n                                      );\n                                    })}\n                                  </ul>\n                                </motion.div>\n                              )}\n                            </AnimatePresence>\n                          </td>\n                        </tr>\n                      </Fragment>\n                    );\n                  })}\n                </tbody>\n              ))\n            )}\n          </table>\n          {!loading && visible.length === 0 && (\n            <div className=\"flex flex-col items-center gap-2 px-6 py-14 text-center\">\n              <p className={cn(\"text-[14px] font-medium\", palette.primaryText)}>\n                {sorted.length === 0 ? \"No releases yet\" : \"No releases match\"}\n              </p>\n              <p className={cn(\"max-w-[320px] text-[13px]\", palette.secondaryText)}>\n                {sorted.length === 0\n                  ? \"Published releases will show up here.\"\n                  : needle\n                    ? `Nothing mentions “${query.trim()}” with the current filters.`\n                    : \"No release has changes of the selected types.\"}\n              </p>\n              {sorted.length > 0 && (\n                <button\n                  type=\"button\"\n                  onClick={clearFilters}\n                  className={cn(\"mt-1 rounded-[8px] border px-3 py-1.5 text-[12.5px] font-medium\", focusRing, palette.control)}\n                >\n                  Clear filters\n                </button>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/changelog-table.tsx"
    },
    {
      "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"
    },
    {
      "path": "components/bjork-ui/_core/canvas.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState, type RefObject } from \"react\";\nimport { isCoarsePointer } from \"./loop\";\n\n// Caps DPR at 1.5 on coarse pointers and 2 otherwise.\nexport function defaultMaxDpr(): number {\n  return isCoarsePointer() ? 1.5 : 2;\n}\n\n// Sets the backing size for a CSS box and returns the DPR used.\nexport function sizeCanvas(\n  canvas: HTMLCanvasElement,\n  cssW: number,\n  cssH: number,\n  maxDpr: number = defaultMaxDpr(),\n): number {\n  const dpr = Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, maxDpr);\n  canvas.width = Math.max(1, Math.round(cssW * dpr));\n  canvas.height = Math.max(1, Math.round(cssH * dpr));\n  canvas.style.width = `${cssW}px`;\n  canvas.style.height = `${cssH}px`;\n  return dpr;\n}\n\n// Rounded size of an element, kept current with a ResizeObserver.\nexport function useElementSize(ref: RefObject<HTMLElement | null>): { width: number; height: number } {\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const box = entries[0]?.contentRect;\n      if (!box) return;\n      const width = Math.round(box.width);\n      const height = Math.round(box.height);\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return size;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/canvas.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/tables/table-theme.ts",
      "content": "import { useEffect, useState } from \"react\";\n\nexport type BjorkTableThemeMode = \"light\" | \"dark\" | \"auto\";\n\nexport function useBjorkTableIsDark(\n  resolvedTheme?: string,\n  forcedTheme?: Exclude<BjorkTableThemeMode, \"auto\">\n) {\n  const [documentTheme, setDocumentTheme] = useState<\"light\" | \"dark\" | null>(null);\n  const [previewTheme, setPreviewTheme] = useState<\"light\" | \"dark\" | null>(null);\n  // Server and first client render must agree, otherwise React keeps the\n  // server-rendered (dark) classes when the real theme resolves to light.\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    const syncDocumentTheme = () => {\n      const root = document.documentElement;\n      const searchParams = new URLSearchParams(window.location.search);\n      const theme = searchParams.get(\"theme\");\n\n      setPreviewTheme(theme === \"light\" || theme === \"dark\" ? theme : null);\n\n      setDocumentTheme(\n        root.classList.contains(\"light\")\n          ? \"light\"\n          : root.classList.contains(\"dark\")\n            ? \"dark\"\n            : null\n      );\n    };\n\n    syncDocumentTheme();\n\n    const observer = new MutationObserver(syncDocumentTheme);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, []);\n\n  const activeTheme =\n    forcedTheme ?? previewTheme ?? documentTheme ?? (mounted ? resolvedTheme : undefined);\n\n  return activeTheme !== \"light\";\n}\n\nexport function getBjorkTablePalette(isDark: boolean) {\n  return {\n    container: isDark\n      ? \"border-[#1c1c1c] bg-[#121212] shadow-[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      : \"border-[#f5ede2] bg-[#fffcf6] shadow-[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    header: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/42\"\n      : \"border-[#f1e8dc] bg-[#fbf7ef] text-[#171717]/60\",\n    row: isDark\n      ? \"border-[#232323] hover:bg-[#181818]/70\"\n      : \"border-[#f1e8dc] hover:bg-[#f8f2e7]/72\",\n    selectedRow: isDark\n      ? \"border-[#2a2a2a] bg-[#181818]\"\n      : \"border-[#eadfce] bg-[#f8f2e7]\",\n    control: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/70 hover:bg-[#202020] shadow-[inset_0_1px_0_rgba(255,255,255,0.045)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/68 shadow-[var(--bjork-shadow-soft)] hover:bg-[#f8f2e7]\",\n    menu: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/76 shadow-[inset_0_1px_0_rgba(255,255,255,0.055),inset_0_12px_24px_rgba(255,255,255,0.018),0_22px_42px_-22px_rgba(0,0,0,0.9)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/76 shadow-[inset_0_1px_0_rgba(88,72,49,0.04),inset_0_12px_24px_rgba(88,72,49,0.022),0_22px_44px_-26px_rgba(66,52,33,0.22)]\",\n    menuItem: isDark ? \"hover:bg-[#232323]\" : \"hover:bg-[#f8f2e7]\",\n    menuActive: isDark ? \"bg-[#232323]\" : \"bg-[#f8f2e7]\",\n    divider: isDark ? \"border-[#232323]\" : \"border-[#f1e8dc]\",\n    primaryText: isDark ? \"text-[#ededed]/90\" : \"text-[#171717]/86\",\n    secondaryText: isDark ? \"text-[#ededed]/38\" : \"text-[#171717]/60\",\n    mutedSurface: isDark ? \"bg-[#181818]\" : \"bg-[#f8f2e7]\",\n    popover: isDark ? \"bg-[#111] text-[#ededed]\" : \"bg-[#fffcf6] text-[#171717]\",\n    accent: isDark ? \"text-[#d86a2c]\" : \"text-[#bd4514]\",\n    accentBg: isDark ? \"bg-[#ec5c13]/12\" : \"bg-[#ec5c13]/14\",\n    accentBorder: isDark ? \"border-[#ec5c13]/24\" : \"border-[#bd4514]/22\",\n    checkboxAccent: isDark ? \"#d86a2c\" : \"#bd4514\",\n  };\n}\n\nexport function getBjorkSignalPalette(\n  tone: \"green\" | \"red\" | \"orange\" | \"neutral\",\n  isDark: boolean\n) {\n  const palettes = {\n    green: {\n      bgColor: isDark ? \"bg-[#3e6f52]/18\" : \"bg-[#dce8dc]\",\n      borderColor: isDark ? \"border-[#5f9f78]/28\" : \"border-[#bad0bd]\",\n      textColor: isDark ? \"text-[#77b58b]\" : \"text-[#47785c]\",\n      dotColor: isDark ? \"bg-[#77b58b]\" : \"bg-[#47785c]\",\n      color: isDark ? \"#77b58b\" : \"#47785c\",\n    },\n    red: {\n      bgColor: isDark ? \"bg-[#87463f]/18\" : \"bg-[#eadad5]\",\n      borderColor: isDark ? \"border-[#c46a61]/28\" : \"border-[#d7bcb4]\",\n      textColor: isDark ? \"text-[#cf7c72]\" : \"text-[#9d5149]\",\n      dotColor: isDark ? \"bg-[#cf7c72]\" : \"bg-[#9d5149]\",\n      color: isDark ? \"#cf7c72\" : \"#9d5149\",\n    },\n    orange: {\n      bgColor: isDark ? \"bg-[#ec5c13]/14\" : \"bg-[#ead9cb]\",\n      borderColor: isDark ? \"border-[#ec5c13]/28\" : \"border-[#d5b59f]\",\n      textColor: isDark ? \"text-[#d86a2c]\" : \"text-[#9f4317]\",\n      dotColor: isDark ? \"bg-[#d86a2c]\" : \"bg-[#9f4317]\",\n      color: isDark ? \"#d86a2c\" : \"#9f4317\",\n    },\n    neutral: {\n      bgColor: isDark ? \"bg-[#ededed]/8\" : \"bg-[#ded7ca]\",\n      borderColor: isDark ? \"border-[#ededed]/14\" : \"border-[#cfc6b7]\",\n      textColor: isDark ? \"text-[#ededed]/58\" : \"text-[#171717]/60\",\n      dotColor: isDark ? \"bg-[#ededed]/45\" : \"bg-[#171717]/42\",\n      color: isDark ? \"#a3a3a3\" : \"#706b62\",\n    },\n  } as const;\n\n  return palettes[tone];\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/table-theme.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": "Tables",
    "registryUrl": "https://ui.isaiahbjork.com/changelog-table.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/changelog-table.json"
  },
  "type": "registry:component"
}