{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radial-command-ring",
  "title": "Radial command ring",
  "description": "Radial command ring is a production-ready navigation component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/navigation/radial-command-ring.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n  type MouseEvent as ReactMouseEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useSpring,\n} from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHidden } from \"@/components/bjork-ui/_core/a11y\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { isCoarsePointer, useVisibleLoop } from \"@/components/bjork-ui/_core/loop\";\nimport { useElementSize } from \"@/components/bjork-ui/_core/canvas\";\nimport { springs } from \"@/components/bjork-ui/_core/motion\";\n\nexport interface RadialCommandItem {\n  id: string;\n  label: string;\n  icon: ReactNode;\n  shortcut?: string;\n  disabled?: boolean;\n}\n\nexport type RadialCommandOpenOn = \"hold\" | \"contextmenu\" | \"both\";\n\nexport interface RadialCommandRingProps {\n  items: RadialCommandItem[];\n  onSelect: (id: string) => void;\n  children?: ReactNode;\n  openOn?: RadialCommandOpenOn;\n  holdMs?: number;\n  radius?: number;\n  innerRadius?: number;\n  deadZone?: number;\n  startAngle?: number;\n  showLabels?: boolean;\n  haptics?: boolean;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Fixes the ring centre (px in the root). Applies when `open` is controlled. */\n  anchor?: { x: number; y: number };\n  /** Controlled highlight. Used for posed previews and tests. */\n  highlightedId?: string | null;\n  tone?: BjorkTone;\n  attract?: boolean;\n  className?: string;\n}\n\ntype Mode = \"hold\" | \"sticky\" | \"keyboard\" | \"attract\";\n\ninterface RingState {\n  open: boolean;\n  x: number | null;\n  y: number | null;\n  highlight: number | null;\n  mode: Mode;\n  coarse: boolean;\n  flash: boolean;\n}\n\nconst CLOSED_RING: RingState = {\n  open: false,\n  x: null,\n  y: null,\n  highlight: null,\n  mode: \"sticky\",\n  coarse: false,\n  flash: false,\n};\n\ninterface Snapshot {\n  items: RadialCommandItem[];\n  n: number;\n  startAngle: number;\n  deadZone: number;\n  holdMs: number;\n  openOn: RadialCommandOpenOn;\n  haptics: boolean;\n  controlled: boolean;\n  openProp: boolean | undefined;\n  onSelect: (id: string) => void;\n  onOpenChange: ((open: boolean) => void) | undefined;\n  radiusProp: number | undefined;\n  geom: { open: boolean; mode: Mode; x: number; y: number; radius: number; c: number };\n}\n\nconst MAX_ITEMS = 8;\nconst MIN_ITEMS = 3;\n// Room around the ring for outer labels. The container is 2 * (radius + PAD) square.\nconst PAD = 120;\nconst HOLD_CANCEL_PX = 8;\nconst FLASH_MS = 90;\nconst EDGE_GAP = 12;\nconst ATTRACT_CYCLE_MS = 3200;\nconst ATTRACT_IDLE_MS = 4000;\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\nconst OPEN_SPRING = { type: \"spring\", stiffness: 420, damping: 32, mass: 0.7 } as const;\nconst ROTATE_SPRING = { stiffness: 520, damping: 38, mass: 0.6 };\nconst CLOSE_TRANSITION = { duration: 0.12, ease: EASE_OUT } as const;\n// Opacity gets its own short ease-out so it reaches 1 within 260ms. The spring stays on scale.\nconst OPEN_FADE = { duration: 0.2, ease: EASE_OUT } as const;\n\n// Event-time clock. Kept out of render so the purity rule stays quiet.\nconst eventNow = () => performance.now();\n\nfunction hexToRgba(hex: string, alpha: number) {\n  const n = parseInt(hex.slice(1), 16);\n  return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${alpha})`;\n}\n\nfunction normDeg(d: number) {\n  return ((d % 360) + 360) % 360;\n}\n\n// Round to half a pixel so icon centres land on a stable grid.\nfunction roundHalf(v: number) {\n  return Math.round(v * 2) / 2;\n}\n\nfunction polar(cx: number, cy: number, r: number, deg: number): [number, number] {\n  const a = (deg * Math.PI) / 180;\n  return [cx + r * Math.cos(a), cy + r * Math.sin(a)];\n}\n\n// Direction, not target: the slice under the pointer, measured from the centre.\nfunction indexAt(dx: number, dy: number, n: number, startAngle: number, deadZone: number) {\n  if (n === 0 || Math.hypot(dx, dy) < deadZone) return null;\n  const step = 360 / n;\n  const theta = (Math.atan2(dy, dx) * 180) / Math.PI;\n  const rel = normDeg(theta - startAngle + step / 2);\n  return Math.min(n - 1, Math.floor(rel / step));\n}\n\nfunction wedgePath(c: number, ri: number, ro: number, step: number, gap: number, midR: number) {\n  const half = ((step / 2) * Math.PI) / 180;\n  const d = gap / midR; // equal arc length at the mid radius, on each side\n  const a0 = -half + d;\n  const a1 = half - d;\n  const pt = (r: number, a: number) =>\n    `${(c + r * Math.cos(a)).toFixed(2)} ${(c + r * Math.sin(a)).toFixed(2)}`;\n  const large = a1 - a0 > Math.PI ? 1 : 0;\n  return `M ${pt(ri, a0)} L ${pt(ro, a0)} A ${ro} ${ro} 0 ${large} 1 ${pt(ro, a1)} L ${pt(ri, a1)} A ${ri} ${ri} 0 ${large} 0 ${pt(ri, a0)} Z`;\n}\n\nfunction attractPhase(t: number, n: number) {\n  if (t >= 2100) return { open: false, highlight: null as number | null };\n  const h = t < 300 ? null : t < 900 ? 0 : t < 1500 ? 1 : 2;\n  return { open: true, highlight: h === null ? null : Math.min(h, n - 1) };\n}\n\nfunction clampCentre(x: number, y: number, radius: number, w: number, h: number) {\n  const m = radius + EDGE_GAP;\n  const cx = w >= 2 * m ? Math.min(Math.max(x, m), w - m) : w / 2;\n  const cy = h >= 2 * m ? Math.min(Math.max(y, m), h - m) : h / 2;\n  return { x: cx, y: cy };\n}\n\nexport function RadialCommandRing({\n  items: rawItems,\n  onSelect,\n  children,\n  openOn = \"both\",\n  holdMs = 220,\n  radius: radiusProp,\n  innerRadius = 44,\n  deadZone = 28,\n  startAngle = -90,\n  showLabels = true,\n  haptics = true,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  anchor,\n  highlightedId,\n  tone,\n  attract = false,\n  className,\n}: RadialCommandRingProps) {\n  const resolvedTone = useBjorkTone(tone);\n  const p = BJORK_PALETTE[resolvedTone];\n  const dark = resolvedTone === \"dark\";\n  const reduce = useReducedMotion() ?? false;\n  const uid = useId();\n  const controlled = openProp !== undefined;\n\n  const items = useMemo(() => rawItems.slice(0, MAX_ITEMS), [rawItems]);\n  const n = items.length;\n  const step = n > 0 ? 360 / n : 360;\n\n  useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return;\n    if (rawItems.length > MAX_ITEMS) {\n      console.warn(`RadialCommandRing takes 3 to 8 items. Rendering the first ${MAX_ITEMS} of ${rawItems.length}.`);\n    } else if (rawItems.length < MIN_ITEMS) {\n      console.warn(`RadialCommandRing takes 3 to 8 items. Got ${rawItems.length}.`);\n    }\n  }, [rawItems.length]);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const surfaceRef = useRef<HTMLDivElement>(null);\n  const menuRef = useRef<HTMLDivElement>(null);\n  const tetherRef = useRef<SVGLineElement>(null);\n  const wedgeRef = useRef<SVGGElement>(null);\n  const size = useElementSize(rootRef);\n\n  const [ring, setRing] = useState<RingState>(() => ({ ...CLOSED_RING, open: defaultOpen && !controlled }));\n  const ringRef = useRef<RingState>(ring);\n  const live = useRef<Snapshot>(null as unknown as Snapshot);\n  const closeRef = useRef<() => void>(() => {});\n  const pointerRef = useRef<{ x: number; y: number } | null>(null);\n  const rectRef = useRef<DOMRect | null>(null);\n  const holdRef = useRef<{ id: number; timer: number; x0: number; y0: number; x: number; y: number; fired: boolean } | null>(null);\n  const captureRef = useRef<number | null>(null);\n  const flashTimer = useRef(0);\n  const busyUntil = useRef(0);\n  const focusOnOpen = useRef(false);\n  const unwrapped = useRef(startAngle);\n  const attractClock = useRef<number | null>(null);\n  const attractKey = useRef(\"\");\n\n  const commit = useCallback((patch: Partial<RingState>) => {\n    const next = { ...ringRef.current, ...patch };\n    ringRef.current = next;\n    setRing(next);\n  }, []);\n\n  const cx = size.width / 2;\n  const cy = size.height / 2;\n  const hi = highlightedId === undefined ? ring.highlight : items.findIndex((it) => it.id === highlightedId);\n  const staticAttract = attract && reduce && !controlled;\n  const attractEnabled = attract && !reduce && !controlled;\n\n  // Shown state. Controlled `open` wins. Reduced-motion attract shows one static frame.\n  let shown = {\n    open: controlled ? !!openProp : ring.open,\n    x: (controlled && anchor ? anchor.x : ring.x) ?? cx,\n    y: (controlled && anchor ? anchor.y : ring.y) ?? cy,\n    highlight: hi === -1 ? null : hi,\n    mode: ring.mode,\n    radius: radiusProp ?? (ring.coarse ? 128 : 112),\n    flash: ring.flash,\n  };\n  if (staticAttract && !ring.open) {\n    shown = { ...shown, open: true, x: cx, y: cy, highlight: n > 1 ? 1 : 0, mode: \"attract\", radius: radiusProp ?? 112, flash: false };\n  }\n\n  const R = shown.radius;\n  const c = R + PAD;\n  const S = 2 * c;\n  const midR = (innerRadius + R) / 2;\n  const hubR = innerRadius - 6;\n  const itemId = (i: number) => `${uid}-item-${i}`;\n  const shownItem = shown.highlight !== null ? items[shown.highlight] : undefined;\n\n  useLayoutEffect(() => {\n    if (controlled) ringRef.current = { ...ringRef.current, open: !!openProp };\n    live.current = {\n      items,\n      n,\n      startAngle,\n      deadZone,\n      holdMs,\n      openOn,\n      haptics,\n      controlled,\n      openProp,\n      onSelect,\n      onOpenChange,\n      radiusProp,\n      geom: { open: shown.open, mode: shown.mode, x: shown.x, y: shown.y, radius: R, c },\n    };\n  });\n\n  // Rotation of the highlight wedge. A motion value, so the spring never re-renders React.\n  const rot = useMotionValue(startAngle);\n  const rotSpring = useSpring(rot, ROTATE_SPRING);\n  const rotation = reduce ? rot : rotSpring;\n\n  useMotionValueEvent(rotation, \"change\", (v) => {\n    const el = wedgeRef.current;\n    if (el) el.setAttribute(\"transform\", `rotate(${v} ${live.current.geom.c} ${live.current.geom.c})`);\n  });\n\n  useLayoutEffect(() => {\n    const el = wedgeRef.current;\n    if (el) el.setAttribute(\"transform\", `rotate(${rotation.get()} ${c} ${c})`);\n  }, [shown.open, c, rotation]);\n\n  const aim = useCallback(\n    (h: number) => {\n      const cur = unwrapped.current;\n      const target = startAngle + h * step;\n      const delta = (((target - cur) % 360) + 540) % 360 - 180; // shortest path\n      unwrapped.current = cur + delta;\n      rot.set(unwrapped.current);\n    },\n    [rot, startAngle, step],\n  );\n\n  useEffect(() => {\n    if (shown.highlight !== null) aim(shown.highlight);\n  }, [shown.highlight, aim]);\n\n  // The tether runs from the hub edge toward the pointer. It is set through refs only.\n  const syncTether = () => {\n    const el = tetherRef.current;\n    if (!el) return;\n    const g = live.current.geom;\n    const pt = pointerRef.current;\n    const hidden = !pt || !g.open || g.mode === \"attract\";\n    const dx = hidden ? 0 : pt.x - g.x;\n    const dy = hidden ? 0 : pt.y - g.y;\n    const d = Math.hypot(dx, dy);\n    if (hidden || d < hubR) {\n      el.setAttribute(\"x1\", String(g.c));\n      el.setAttribute(\"y1\", String(g.c));\n      el.setAttribute(\"x2\", String(g.c));\n      el.setAttribute(\"y2\", String(g.c));\n      return;\n    }\n    const ux = dx / d;\n    const uy = dy / d;\n    const end = Math.min(d, g.radius);\n    el.setAttribute(\"x1\", (g.c + ux * hubR).toFixed(2));\n    el.setAttribute(\"y1\", (g.c + uy * hubR).toFixed(2));\n    el.setAttribute(\"x2\", (g.c + ux * end).toFixed(2));\n    el.setAttribute(\"y2\", (g.c + uy * end).toFixed(2));\n  };\n\n  useLayoutEffect(() => {\n    syncTether();\n  }, [shown.open, shown.x, shown.y, R]);\n\n  useLayoutEffect(() => {\n    if (shown.open && focusOnOpen.current) {\n      focusOnOpen.current = false;\n      menuRef.current?.focus({ preventScroll: true });\n    }\n  }, [shown.open]);\n\n  // Attract: a deterministic idle loop. Pauses for 4s after any input inside the component.\n  const attractRunning = attractEnabled;\n  useVisibleLoop(\n    rootRef,\n    (dt) => {\n      const r = ringRef.current;\n      if (r.open && r.mode !== \"attract\") {\n        attractClock.current = null;\n        return true;\n      }\n      if (eventNow() < busyUntil.current) {\n        attractClock.current = null;\n        return true;\n      }\n      if (attractClock.current === null) {\n        attractClock.current = 0;\n        attractKey.current = \"\";\n      }\n      attractClock.current += dt * 1000;\n      const phase = attractPhase(attractClock.current % ATTRACT_CYCLE_MS, live.current.n);\n      const key = `${phase.open}|${phase.highlight}`;\n      if (key !== attractKey.current) {\n        attractKey.current = key;\n        commit({ open: phase.open, x: null, y: null, highlight: phase.highlight, mode: \"attract\", coarse: isCoarsePointer(), flash: false });\n      }\n      return true;\n    },\n    { enabled: attractRunning },\n  );\n\n  const noteInput = () => {\n    busyUntil.current = eventNow() + ATTRACT_IDLE_MS;\n    if (ringRef.current.open && ringRef.current.mode === \"attract\") {\n      commit({ open: false });\n    }\n  };\n\n  const releaseCapture = () => {\n    const id = captureRef.current;\n    captureRef.current = null;\n    const el = rootRef.current;\n    if (id !== null && el && el.hasPointerCapture(id)) el.releasePointerCapture(id);\n  };\n\n  const closeRing = () => {\n    if (!ringRef.current.open) return;\n    const menuHadFocus = menuRef.current?.contains(document.activeElement) ?? false;\n    const wasAttract = ringRef.current.mode === \"attract\";\n    holdRef.current = null;\n    releaseCapture();\n    commit({ open: false, flash: false });\n    if (menuHadFocus) surfaceRef.current?.focus({ preventScroll: true });\n    if (!wasAttract) live.current.onOpenChange?.(false);\n  };\n\n  useLayoutEffect(() => {\n    closeRef.current = closeRing;\n  });\n\n  const activate = (i: number) => {\n    const item = live.current.items[i];\n    if (!item || item.disabled) return;\n    live.current.onSelect(item.id);\n    closeRing();\n  };\n\n  const setHighlight = (h: number | null) => {\n    if (ringRef.current.highlight === h) return;\n    if (h !== null && live.current.haptics) navigator.vibrate?.(4);\n    commit({ highlight: h });\n  };\n\n  const openRing = (mode: Mode, pos: { x: number; y: number } | null, highlight: number | null) => {\n    const L = live.current;\n    const el = rootRef.current;\n    if (!el || L.n === 0) return;\n    const rect = el.getBoundingClientRect();\n    rectRef.current = rect;\n    const coarse = isCoarsePointer();\n    const radius = L.radiusProp ?? (coarse ? 128 : 112);\n    const at = clampCentre(pos?.x ?? rect.width / 2, pos?.y ?? rect.height / 2, radius, rect.width, rect.height);\n    pointerRef.current = pos;\n    const wasOpen = ringRef.current.open;\n    commit({ open: true, x: at.x, y: at.y, highlight, mode, coarse, flash: false });\n    focusOnOpen.current = true;\n    if (!wasOpen) L.onOpenChange?.(true);\n  };\n\n  const fireHold = () => {\n    const h = holdRef.current;\n    if (!h) return;\n    h.fired = true;\n    h.timer = 0;\n    captureRef.current = h.id;\n    try {\n      rootRef.current?.setPointerCapture(h.id);\n    } catch {\n      captureRef.current = null;\n    }\n    openRing(\"hold\", { x: h.x, y: h.y }, null);\n  };\n\n  const localPoint = (e: ReactPointerEvent<HTMLDivElement> | ReactMouseEvent<HTMLDivElement>) => {\n    const rect = rectRef.current ?? e.currentTarget.getBoundingClientRect();\n    return { x: e.clientX - rect.left, y: e.clientY - rect.top };\n  };\n\n  const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n    noteInput();\n    const L = live.current;\n    if (ringRef.current.open) return;\n    if (e.button !== 0 || L.n === 0) return;\n    if (L.openOn !== \"hold\" && L.openOn !== \"both\") return;\n    const rect = e.currentTarget.getBoundingClientRect();\n    rectRef.current = rect;\n    const x = e.clientX - rect.left;\n    const y = e.clientY - rect.top;\n    holdRef.current = {\n      id: e.pointerId,\n      timer: window.setTimeout(fireHold, L.holdMs),\n      x0: x,\n      y0: y,\n      x,\n      y,\n      fired: false,\n    };\n  };\n\n  const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n    noteInput();\n    const pt = localPoint(e);\n    const h = holdRef.current;\n    if (h && !h.fired) {\n      h.x = pt.x;\n      h.y = pt.y;\n      if (Math.hypot(pt.x - h.x0, pt.y - h.y0) > HOLD_CANCEL_PX) {\n        window.clearTimeout(h.timer);\n        holdRef.current = null;\n      }\n      return;\n    }\n    if (!ringRef.current.open || ringRef.current.flash) return;\n    pointerRef.current = pt;\n    const r = ringRef.current;\n    const gx = r.x ?? cx;\n    const gy = r.y ?? cy;\n    setHighlight(indexAt(pt.x - gx, pt.y - gy, live.current.n, startAngle, deadZone));\n    syncTether();\n  };\n\n  const onPointerUp = () => {\n    const h = holdRef.current;\n    if (h && !h.fired) {\n      window.clearTimeout(h.timer);\n      holdRef.current = null;\n      return;\n    }\n    const r = ringRef.current;\n    if (!r.open || r.mode !== \"hold\" || r.flash) return;\n    holdRef.current = null;\n    releaseCapture();\n    const idx = r.highlight;\n    const item = idx !== null ? live.current.items[idx] : undefined;\n    if (idx !== null && item && !item.disabled) {\n      commit({ flash: true });\n      flashTimer.current = window.setTimeout(() => {\n        flashTimer.current = 0;\n        activate(idx);\n      }, FLASH_MS);\n    } else {\n      closeRing();\n    }\n  };\n\n  const onPointerCancel = () => {\n    const h = holdRef.current;\n    if (h && !h.fired) {\n      window.clearTimeout(h.timer);\n      holdRef.current = null;\n      return;\n    }\n    if (ringRef.current.open && ringRef.current.mode === \"hold\") closeRing();\n  };\n\n  const onContextMenu = (e: ReactMouseEvent<HTMLDivElement>) => {\n    const L = live.current;\n    if (L.openOn !== \"contextmenu\" && L.openOn !== \"both\") return;\n    e.preventDefault();\n    noteInput();\n    const pt = localPoint(e);\n    if (ringRef.current.open) {\n      commit({ x: pt.x, y: pt.y });\n      pointerRef.current = pt;\n      return;\n    }\n    openRing(\"sticky\", pt, null);\n  };\n\n  const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    noteInput();\n    const L = live.current;\n    const key = e.key;\n    const n = L.n;\n    if (!ringRef.current.open) {\n      if (key === \"Enter\" || key === \" \" || key === \"ContextMenu\" || (key === \"F10\" && e.shiftKey)) {\n        e.preventDefault();\n        if (e.repeat) return;\n        const first = L.items.findIndex((it) => !it.disabled);\n        const el = rootRef.current;\n        if (!el) return;\n        openRing(\"keyboard\", null, first === -1 ? 0 : first);\n      }\n      return;\n    }\n    const h = ringRef.current.highlight;\n    if (ringRef.current.flash) {\n      e.preventDefault();\n      return;\n    }\n    switch (key) {\n      case \"Escape\":\n        e.preventDefault();\n        closeRing();\n        return;\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        e.preventDefault();\n        setHighlight(h === null ? 0 : (h + 1) % n);\n        return;\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        e.preventDefault();\n        setHighlight(h === null ? n - 1 : (h - 1 + n) % n);\n        return;\n      case \"Home\":\n        e.preventDefault();\n        setHighlight(0);\n        return;\n      case \"End\":\n        e.preventDefault();\n        setHighlight(n - 1);\n        return;\n      case \"Enter\":\n      case \" \":\n        e.preventDefault();\n        if (e.repeat || h === null) return;\n        activate(h);\n        return;\n      default:\n        break;\n    }\n    if (/^[1-8]$/.test(key)) {\n      const idx = Number(key) - 1;\n      if (idx < n) {\n        e.preventDefault();\n        activate(idx);\n      }\n      return;\n    }\n    if (key.length === 1 && /[a-z]/i.test(key) && !e.ctrlKey && !e.metaKey && !e.altKey) {\n      const want = key.toLowerCase();\n      for (let k = 1; k <= n; k += 1) {\n        const j = ((h ?? -1) + k) % n;\n        if (L.items[j].label.trim().toLowerCase().startsWith(want)) {\n          e.preventDefault();\n          setHighlight(j);\n          return;\n        }\n      }\n    }\n  };\n\n  // Click outside closes a sticky or keyboard-opened ring.\n  const outsideActive = ring.open && (ring.mode === \"sticky\" || ring.mode === \"keyboard\");\n  useEffect(() => {\n    if (!outsideActive) return;\n    const onDown = (e: PointerEvent) => {\n      if (!menuRef.current?.contains(e.target as Node)) closeRef.current();\n    };\n    document.addEventListener(\"pointerdown\", onDown, true);\n    return () => document.removeEventListener(\"pointerdown\", onDown, true);\n  }, [outsideActive]);\n\n  useEffect(() => {\n    const flash = flashTimer;\n    const hold = holdRef;\n    return () => {\n      window.clearTimeout(flash.current);\n      if (hold.current) window.clearTimeout(hold.current.timer);\n    };\n  }, []);\n\n  const discBg = dark ? \"rgba(18,18,18,0.92)\" : \"rgba(255,252,246,0.94)\";\n  const discBorder = dark ? p.border : p.borderStrong;\n  const discShadow = dark\n    ? \"0 24px 48px -16px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05)\"\n    : \"0 24px 48px -20px rgba(66,52,33,.25)\";\n  const wedgeFill = hexToRgba(p.accent, shown.flash ? 0.3 : 0.16);\n  const wedgeStroke = hexToRgba(p.accent, dark ? 0.55 : 0.6);\n  const wedgeD = wedgePath(c, innerRadius + 4, R - 4, step, 2, midR);\n  const hairPath = Array.from({ length: n }, (_, k) => {\n    const a = startAngle - step / 2 + k * step;\n    const [x1, y1] = polar(c, c, innerRadius, a);\n    const [x2, y2] = polar(c, c, R, a);\n    return `M${x1.toFixed(2)} ${y1.toFixed(2)}L${x2.toFixed(2)} ${y2.toFixed(2)}`;\n  }).join(\"\");\n\n  let labelAnchor: \"start\" | \"middle\" | \"end\" = \"middle\";\n  let labelX = c;\n  let labelY = c;\n  if (showLabels && shown.highlight !== null) {\n    const theta = startAngle + shown.highlight * step;\n    const cosT = Math.cos((theta * Math.PI) / 180);\n    labelAnchor = Math.abs(cosT) < Math.sin((15 * Math.PI) / 180) ? \"middle\" : cosT < 0 ? \"end\" : \"start\";\n    [labelX, labelY] = polar(c, c, R + 18, theta);\n  }\n  // The outer label renders only when it fits inside the root. The hub already names the item.\n  const lw = shownItem ? shownItem.label.length * 7 + 4 : 0;\n  const lx = shown.x - c + labelX;\n  const labelFits =\n    labelAnchor === \"start\"\n      ? lx + lw <= size.width - 4\n      : labelAnchor === \"end\"\n        ? lx - lw >= 4\n        : lx - lw / 2 >= 4 && lx + lw / 2 <= size.width - 4;\n\n  const ringStyle: CSSProperties = {\n    left: shown.x - c,\n    top: shown.y - c,\n    width: S,\n    height: S,\n  };\n\n  const hubHead = shownItem ? shownItem.label : shown.mode === \"hold\" ? \"Release to cancel\" : \"Escape to close\";\n\n  return (\n    <div\n      ref={rootRef}\n      data-tone={resolvedTone}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={onPointerCancel}\n      onContextMenu={onContextMenu}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"relative select-none\",\n        className,\n      )}\n      style={{\n        WebkitTouchCallout: \"none\",\n        touchAction: shown.open ? \"none\" : undefined,\n      } as CSSProperties}\n    >\n      <div\n        ref={surfaceRef}\n        role=\"button\"\n        tabIndex={0}\n        aria-haspopup=\"menu\"\n        aria-expanded={shown.open}\n        aria-label=\"Actions\"\n        className=\"pointer-events-none absolute inset-0 z-0 rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--bjork-accent)]\"\n      >\n        <VisuallyHidden>Press Enter for actions</VisuallyHidden>\n      </div>\n      {children}\n      <AnimatePresence>\n        {shown.open && (\n          <motion.div\n            key=\"ring\"\n            ref={menuRef}\n            role=\"menu\"\n            aria-label=\"Actions\"\n            tabIndex={-1}\n            aria-activedescendant={shown.highlight !== null ? itemId(shown.highlight) : undefined}\n            className=\"pointer-events-none absolute outline-none\"\n            style={ringStyle}\n            initial={{ opacity: 0, scale: reduce ? 1 : 0.92 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: reduce ? 1 : 0.96, transition: CLOSE_TRANSITION }}\n            transition={reduce ? CLOSE_TRANSITION : { scale: OPEN_SPRING, opacity: OPEN_FADE }}\n          >\n            <div\n              className=\"absolute rounded-full border backdrop-blur-[16px]\"\n              style={{\n                left: PAD,\n                top: PAD,\n                width: 2 * R,\n                height: 2 * R,\n                background: discBg,\n                borderColor: discBorder,\n                boxShadow: discShadow,\n              }}\n            />\n            <svg\n              aria-hidden=\"true\"\n              width={S}\n              height={S}\n              viewBox={`0 0 ${S} ${S}`}\n              className=\"absolute left-0 top-0\"\n              style={{ overflow: \"visible\" }}\n            >\n              <g ref={wedgeRef} style={{ opacity: shown.highlight === null ? 0 : 1 }}>\n                <path d={wedgeD} fill={wedgeFill} stroke={wedgeStroke} strokeWidth={1} />\n              </g>\n              <path d={hairPath} stroke={p.hair} strokeWidth={1} fill=\"none\" />\n              <line ref={tetherRef} stroke={p.textFaint} strokeWidth={1} />\n              {shownItem && labelAnchor !== undefined && labelFits && (\n                <motion.text\n                  key={shown.highlight ?? -1}\n                  x={labelX}\n                  y={labelY}\n                  textAnchor={labelAnchor}\n                  dominantBaseline=\"middle\"\n                  fill={p.text}\n                  className=\"font-bjork-alpha\"\n                  style={{ fontSize: 12, fontWeight: 500 }}\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={CLOSE_TRANSITION}\n                >\n                  {shownItem.label}\n                </motion.text>\n              )}\n            </svg>\n            <div\n              className=\"absolute grid place-items-center rounded-full border\"\n              style={{\n                left: c - hubR,\n                top: c - hubR,\n                width: 2 * hubR,\n                height: 2 * hubR,\n                background: p.surface,\n                borderColor: p.border,\n              }}\n            >\n              <div className=\"flex w-[68px] flex-col items-center text-center\">\n                <span\n                  className=\"block max-h-[26px] overflow-hidden font-mono text-[11px] uppercase leading-[13px] tracking-[0.08em] [text-box:trim-both_cap_alphabetic]\"\n                  style={{ color: p.text }}\n                >\n                  {hubHead}\n                </span>\n                {shownItem?.shortcut && (\n                  <span className=\"mt-1.5 font-mono text-[10px] leading-none tabular-nums\" style={{ color: p.textFaint }}>\n                    {shownItem.shortcut}\n                  </span>\n                )}\n              </div>\n            </div>\n            {items.map((item, i) => {\n              const [ix, iy] = polar(c, c, midR, startAngle + i * step);\n              const active = shown.highlight === i;\n              return (\n                <button\n                  key={item.id}\n                  id={itemId(i)}\n                  type=\"button\"\n                  role=\"menuitem\"\n                  tabIndex={-1}\n                  aria-label={item.label}\n                  aria-disabled={item.disabled ? true : undefined}\n                  onClick={() => {\n                    if (ringRef.current.mode === \"hold\") return;\n                    activate(i);\n                  }}\n                  className=\"pointer-events-auto absolute grid size-11 place-items-center rounded-full outline-none\"\n                  style={{\n                    left: roundHalf(ix),\n                    top: roundHalf(iy),\n                    transform: \"translate(-50%,-50%)\",\n                    color: active ? p.text : p.textMuted,\n                    background: \"transparent\",\n                    border: 0,\n                    padding: 0,\n                  }}\n                >\n                  <motion.span\n                    className=\"grid place-items-center\"\n                    initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    transition={reduce ? CLOSE_TRANSITION : { ...springs.snappy, delay: i * 0.012 }}\n                  >\n                    <span\n                      className=\"grid place-items-center [&_svg]:size-5\"\n                      style={{\n                        transform: active ? \"scale(1.08)\" : undefined,\n                        opacity: item.disabled ? 0.3 : 1,\n                      }}\n                    >\n                      {item.icon}\n                    </span>\n                  </motion.span>\n                </button>\n              );\n            })}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/navigation/radial-command-ring.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/a11y.tsx",
      "content": "import type { ReactNode } from \"react\";\n\n// Text for assistive tech only.\nexport function VisuallyHidden({ children }: { children: ReactNode }) {\n  return <span className=\"sr-only\">{children}</span>;\n}\n\n// Announces `message` when it changes. Same text re-rendered does not re-announce.\nexport function LiveRegion({\n  message,\n  politeness = \"polite\",\n}: {\n  message: string;\n  politeness?: \"polite\" | \"assertive\";\n}) {\n  return (\n    <span role=\"status\" aria-live={politeness} aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/a11y.tsx"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/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/_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/motion.ts",
      "content": "// Shared springs, easings and durations. Components import these instead of inventing values.\n\nexport const springs = {\n  press: { type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 },\n  snappy: { type: \"spring\", stiffness: 400, damping: 25, mass: 1 },\n  standard: { type: \"spring\", stiffness: 300, damping: 30, mass: 0.8 },\n  soft: { type: \"spring\", stiffness: 200, damping: 25, mass: 1.2 },\n  settle: { type: \"spring\", stiffness: 380, damping: 30, mass: 0.8 },\n  blurIn: { type: \"spring\", stiffness: 400, damping: 28, mass: 0.6 },\n} as const;\n\nexport const ease = {\n  out: [0.23, 1, 0.32, 1],\n  inOut: [0.77, 0, 0.175, 1],\n  drawer: [0.32, 0.72, 0, 1],\n} as const;\n\nexport const easeCss = {\n  out: \"cubic-bezier(0.23,1,0.32,1)\",\n  inOut: \"cubic-bezier(0.77,0,0.175,1)\",\n  drawer: \"cubic-bezier(0.32,0.72,0,1)\",\n} as const;\n\nexport const durations = { press: 140, tooltip: 160, menu: 200, panel: 280 } as const; // ms\n\n// Cubic bezier easing for canvas and rAF code. Returns progress y for input x in [0, 1].\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (s: number) => ((ax * s + bx) * s + cx) * s;\n  const sampleY = (s: number) => ((ay * s + by) * s + cy) * s;\n  const sampleDX = (s: number) => (3 * ax * s + 2 * bx) * s + cx;\n\n  const solveX = (x: number) => {\n    let s = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(s) - x;\n      if (Math.abs(err) < 1e-6) return s;\n      const d = sampleDX(s);\n      if (Math.abs(d) < 1e-6) break;\n      s -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    s = x;\n    while (lo < hi) {\n      const v = sampleX(s);\n      if (Math.abs(v - x) < 1e-6) return s;\n      if (x > v) lo = s;\n      else hi = s;\n      s = (lo + hi) / 2;\n      if (hi - lo < 1e-7) break;\n    }\n    return s;\n  };\n\n  return (t: number) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    return sampleY(solveX(t));\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/motion.ts"
    }
  ],
  "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": "Navigation",
    "registryUrl": "https://ui.isaiahbjork.com/radial-command-ring.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/radial-command-ring.json"
  },
  "type": "registry:component"
}