{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "click-wheel",
  "title": "Click wheel",
  "description": "Click wheel is a production-ready controls component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/controls/click-wheel.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useEffect, useId, useRef, useState, type KeyboardEvent, type PointerEvent } from \"react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { cubicBezier, ease, springs } from \"@/components/bjork-ui/_core/motion\";\nimport { stepSpring, type SpringState } from \"@/components/bjork-ui/_core/spring\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { useVisibleLoop } from \"@/components/bjork-ui/_core/loop\";\n\nexport interface ClickWheelItem {\n  id: string;\n  label: string;\n  meta?: string;\n}\n\nexport interface ClickWheelProps {\n  items: ClickWheelItem[];\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (index: number) => void;\n  onSelect?: (item: ClickWheelItem, index: number) => void;\n  onTick?: (index: number) => void;\n  detentsPerRev?: number;\n  inertia?: number;\n  loop?: boolean;\n  haptics?: boolean;\n  layout?: \"stacked\" | \"wheel-only\";\n  size?: number;\n  centerLabel?: string;\n  ariaLabel?: string;\n  tone?: BjorkTone;\n  attract?: boolean;\n  className?: string;\n}\n\n// Stacked layout: 140 display + 60 gap + 220 wheel = 420 tall.\nconst LIST_W = 260;\nconst LIST_H = 140;\nconst ROW_H = 28;\nconst ROW_TOP = (LIST_H - ROW_H) / 2; // the active row sits in the middle, 56px from the top\nconst PILL_INSET = 4;\nconst PILL_RADIUS = 8;\nconst STACK_GAP = 60;\nconst DEFAULT_SIZE = 220;\nconst CENTER_AT_DEFAULT = 92;\nconst FLASH_MS = 160;\nconst TWEEN_MS = 120;\nconst FLICK_SPEED = 8; // items per second\nconst FLICK_EVERY = 4; // seconds of idle before an attract flick\nconst SNAP_SPEED = 0.5; // items per second, below which the wheel snaps to a detent\nconst MAX_FLICK = 6; // items/s, keeps any flick's settle ≤ 1.2s\nconst RELEASE_WINDOW_MS = 80;\nconst SAMPLE_KEEP_MS = 200;\nconst RUBBER = 0.35;\nconst WHEEL_PX_PER_ITEM = 40;\nconst WHEEL_SNAP_DELAY = 140;\nconst FADE_MASK = \"linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%)\";\nconst easeOut = cubicBezier(...ease.out);\n\nfunction mod(a: number, n: number) {\n  return ((a % n) + n) % n;\n}\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v));\n}\n\n// Index shown to the user for an unwrapped step count.\nfunction wrapIndex(k: number, n: number, loop: boolean) {\n  if (n <= 0) return 0;\n  return loop ? mod(k, n) : clamp(k, 0, n - 1);\n}\n\n// Unwrapped step the wheel may rest on. Without loop, the rest positions stop at the ends.\nfunction restStep(k: number, n: number, loop: boolean) {\n  if (n <= 0 || loop) return k;\n  return clamp(k, 0, n - 1);\n}\n\nfunction pointerAngle(x: number, y: number, cx: number, cy: number) {\n  return (Math.atan2(y - cy, x - cx) * 180) / Math.PI;\n}\n\ninterface WheelRowProps {\n  id?: string;\n  option: boolean;\n  selected: boolean;\n  hidden: boolean;\n  label: string;\n  meta?: string;\n  labelColor: string;\n  metaColor: string;\n}\n\n// Memoised so a detent only re-renders the rows whose selected state changed, not the whole list.\nconst WheelRow = memo(function WheelRow({ id, option, selected, hidden, label, meta, labelColor, metaColor }: WheelRowProps) {\n  return (\n    <div\n      id={id}\n      role={option ? \"option\" : undefined}\n      aria-selected={option ? selected : undefined}\n      aria-hidden={hidden || undefined}\n      className=\"flex h-[28px] items-center justify-between gap-3 px-[14px]\"\n    >\n      <span className=\"truncate font-bjork-alpha text-[13px] font-medium\" style={{ color: labelColor }}>\n        {label}\n      </span>\n      {meta && (\n        <span className=\"shrink-0 font-mono text-[11px] tabular-nums\" style={{ color: metaColor }}>\n          {meta}\n        </span>\n      )}\n    </div>\n  );\n});\n\n// Token with a palette fallback, so the component keeps its tone outside the site.\nfunction tok(name: string, fallback: string) {\n  return `var(--bjork-${name}, ${fallback})`;\n}\n\nexport function ClickWheel({\n  items,\n  value,\n  defaultValue = 0,\n  onValueChange,\n  onSelect,\n  onTick,\n  detentsPerRev = 12,\n  inertia = 0.94,\n  loop = false,\n  haptics = true,\n  layout = \"stacked\",\n  size = DEFAULT_SIZE,\n  centerLabel = \"Select\",\n  ariaLabel = \"Click wheel\",\n  tone,\n  attract = false,\n  className,\n}: ClickWheelProps) {\n  const resolvedTone = useBjorkTone(tone);\n  const p = BJORK_PALETTE[resolvedTone];\n  const dark = resolvedTone === \"dark\";\n  const reduced = useReducedMotion();\n  const uid = useId();\n  const listboxId = `${uid}-list`;\n  const optionId = (i: number) => `${uid}-opt-${i}`;\n\n  const n = items.length;\n  const detents = Math.max(1, Math.round(detentsPerRev));\n  const step = 360 / detents;\n  const stacked = layout === \"stacked\";\n  const isControlled = value !== undefined;\n  const attractOn = attract && !reduced && !isControlled && n > 0;\n  const centerD = Math.round((size * CENTER_AT_DEFAULT) / DEFAULT_SIZE);\n  // Loop mode repeats the list so the rows around the centre are always filled.\n  const mid = loop && n > 0 ? Math.ceil(4 / n) : 0;\n  const copies = loop && n > 0 ? 2 * mid + 1 : 1;\n  const rowCount = n * copies;\n\n  // Continuous position in items, plus the last reported (unwrapped) step.\n  const startStep = restStep(value ?? defaultValue, n, loop);\n  const posRef = useRef(startStep);\n  const lastStepRef = useRef(Math.round(startStep));\n  const [index, setIndex] = useState(() => wrapIndex(Math.round(startStep), n, loop));\n  const velRef = useRef(0);\n  const springTarget = useRef<number | null>(null);\n  const springState = useRef<SpringState>({ x: 0, v: 0 });\n  const tween = useRef<{ from: number; to: number; t0: number } | null>(null);\n  const drag = useRef<{ cx: number; cy: number; last: number; samples: { t: number; p: number }[] } | null>(null);\n  const flashes = useRef<{ tick: number; t0: number }[]>([]);\n  const tickEls = useRef<(HTMLSpanElement | null)[]>([]);\n  const listEl = useRef<HTMLDivElement | null>(null);\n  const invEl = useRef<HTMLDivElement | null>(null);\n  const notchEl = useRef<HTMLSpanElement | null>(null);\n  const wheelEl = useRef<HTMLDivElement | null>(null);\n  const rootEl = useRef<HTMLDivElement | null>(null);\n  const wakeRef = useRef<() => void>(() => {});\n  const inputSeen = useRef(false);\n  const idleFor = useRef(0);\n  const flickDir = useRef(1);\n  const wheelSnap = useRef<number | null>(null);\n  const wheelHandler = useRef<(e: WheelEvent) => void>(() => {});\n\n  const kick = useCallback(() => wakeRef.current(), []);\n\n  // Writes transforms straight to the DOM. React never re-renders for a frame of motion.\n  const paint = useCallback(() => {\n    const pos = posRef.current;\n    const wrapped = loop && n > 0 ? mod(pos, n) : pos;\n    const y = `translate3d(0, ${(ROW_TOP - (wrapped + mid * n) * ROW_H).toFixed(2)}px, 0)`;\n    if (listEl.current) listEl.current.style.transform = y;\n    if (invEl.current) invEl.current.style.transform = y;\n    if (notchEl.current) notchEl.current.style.transform = `rotate(${(pos * step).toFixed(3)}deg)`;\n  }, [loop, n, mid, step]);\n\n  // Fires the callbacks for every detent between the last reported step and `k`.\n  const reportTo = (k: number) => {\n    const from = lastStepRef.current;\n    if (k === from || n === 0) return;\n    const dir = k > from ? 1 : -1;\n    const t0 = performance.now();\n    lastStepRef.current = k;\n    for (let s = from + dir; dir > 0 ? s <= k : s >= k; s += dir) {\n      const idx = wrapIndex(s, n, loop);\n      if (haptics && typeof navigator !== \"undefined\" && typeof navigator.vibrate === \"function\") {\n        navigator.vibrate(3);\n      }\n      onTick?.(idx);\n      onValueChange?.(idx);\n      if (!reduced) flashes.current.push({ tick: mod(idx, detents), t0 });\n    }\n    setIndex(wrapIndex(k, n, loop));\n    kick();\n  };\n\n  const stopMotion = () => {\n    velRef.current = 0;\n    springTarget.current = null;\n    tween.current = null;\n  };\n\n  const startSpring = (target: number, v0: number) => {\n    springTarget.current = target;\n    springState.current = { x: posRef.current, v: v0 };\n  };\n\n  const frame = (dt: number) => {\n    const now = performance.now();\n    let busy = false;\n\n    // Attract: flick every few seconds while nobody touches the wheel. Alternates direction.\n    if (attractOn && !drag.current) {\n      if (inputSeen.current) {\n        inputSeen.current = false;\n        idleFor.current = 0;\n      } else {\n        idleFor.current += dt;\n      }\n      if (\n        idleFor.current >= FLICK_EVERY &&\n        !tween.current &&\n        springTarget.current === null &&\n        velRef.current === 0\n      ) {\n        idleFor.current = 0;\n        velRef.current = flickDir.current * FLICK_SPEED;\n        flickDir.current = -flickDir.current;\n      }\n      busy = true;\n    }\n\n    if (drag.current) {\n      busy = true;\n    } else if (tween.current) {\n      const tw = tween.current;\n      const pr = Math.min(1, (now - tw.t0) / TWEEN_MS);\n      posRef.current = tw.from + (tw.to - tw.from) * easeOut(pr);\n      if (pr >= 1) {\n        posRef.current = tw.to;\n        tween.current = null;\n      }\n      busy = true;\n    } else if (springTarget.current !== null) {\n      const target = springTarget.current;\n      const s = springState.current;\n      stepSpring(s, target, springs.settle, dt);\n      posRef.current = s.x;\n      if (Math.abs(s.x - target) < 1e-3 && Math.abs(s.v) < 1e-3) {\n        posRef.current = target;\n        springTarget.current = null;\n        s.v = 0;\n      }\n      busy = true;\n    } else if (velRef.current !== 0) {\n      const v = velRef.current;\n      posRef.current += v * dt;\n      velRef.current = v * Math.pow(inertia, dt * 60);\n      const outside = !loop && n > 0 && (posRef.current < 0 || posRef.current > n - 1);\n      if (outside) {\n        // Past an end without loop: spring back to the end.\n        velRef.current = 0;\n        startSpring(restStep(Math.round(posRef.current), n, loop), v);\n      } else if (Math.abs(velRef.current) < SNAP_SPEED) {\n        // Slow enough: settle on the nearest detent.\n        velRef.current = 0;\n        startSpring(restStep(Math.round(posRef.current), n, loop), 0);\n      }\n      busy = true;\n    }\n\n    if (flashes.current.length > 0) {\n      const keep: { tick: number; t0: number }[] = [];\n      for (const f of flashes.current) {\n        const pr = (now - f.t0) / FLASH_MS;\n        const el = tickEls.current[f.tick];\n        if (pr >= 1) {\n          if (el) el.style.transform = \"\";\n          continue;\n        }\n        // Ticks rest at full opacity, so the flash is a pulse: the tick grows from 6px to 10px and back over 160ms.\n        // This deliberately replaces the plan's opacity flash, which dipped the tick below its resting opacity.\n        if (el) el.style.transform = `scaleY(${(1 + 0.67 * Math.sin(Math.PI * pr)).toFixed(3)})`;\n        keep.push(f);\n      }\n      flashes.current = keep;\n      busy = true;\n    }\n\n    paint();\n    if (!tween.current && !drag.current) {\n      reportTo(restStep(Math.round(posRef.current), n, loop));\n    }\n    return busy;\n  };\n\n  const { wake } = useVisibleLoop(rootEl, frame);\n  useEffect(() => {\n    wakeRef.current = wake;\n  }, [wake]);\n\n  // Keep the DOM transforms in step with the latest render (list, notch).\n  useEffect(() => {\n    paint();\n  });\n\n  // The wheel listens natively, non-passive, so it can stop page scroll while the pointer is over it.\n  useEffect(() => {\n    wheelHandler.current = (e: WheelEvent) => {\n      if (n === 0) return;\n      e.preventDefault();\n      inputSeen.current = true;\n      tween.current = null;\n      springTarget.current = null;\n      velRef.current = 0;\n      posRef.current += e.deltaY / WHEEL_PX_PER_ITEM;\n      paint();\n      reportTo(restStep(Math.round(posRef.current), n, loop));\n      if (wheelSnap.current !== null) window.clearTimeout(wheelSnap.current);\n      wheelSnap.current = window.setTimeout(() => {\n        wheelSnap.current = null;\n        const target = restStep(Math.round(posRef.current), n, loop);\n        if (reduced) {\n          posRef.current = target;\n          paint();\n          return;\n        }\n        startSpring(target, 0);\n        kick();\n      }, WHEEL_SNAP_DELAY);\n    };\n  });\n\n  useEffect(() => {\n    const el = wheelEl.current;\n    if (!el) return;\n    const onWheel = (e: WheelEvent) => wheelHandler.current(e);\n    el.addEventListener(\"wheel\", onWheel, { passive: false });\n    return () => {\n      el.removeEventListener(\"wheel\", onWheel);\n      if (wheelSnap.current !== null) window.clearTimeout(wheelSnap.current);\n    };\n  }, []);\n\n  // Controlled value: jump to it when the parent changes it and the user is not dragging.\n  useEffect(() => {\n    if (value === undefined || n === 0 || drag.current) return;\n    const cur = wrapIndex(lastStepRef.current, n, loop);\n    const want = wrapIndex(value, n, loop);\n    if (cur === want) return;\n    let target = want;\n    if (loop) {\n      let d = mod(want - cur, n);\n      if (d > n / 2) d -= n;\n      target = lastStepRef.current + d;\n    }\n    velRef.current = 0;\n    springTarget.current = null;\n    tween.current = null;\n    posRef.current = target;\n    lastStepRef.current = target;\n    paint();\n    kick();\n  }, [value, n, loop, paint, kick]);\n\n  const selectCurrent = () => {\n    if (n === 0) return;\n    inputSeen.current = true;\n    const idx = wrapIndex(lastStepRef.current, n, loop);\n    onSelect?.(items[idx], idx);\n  };\n\n  const onWheelPointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    if (n === 0 || e.button !== 0) return;\n    // The one measurement on the gesture path. Read once per press, never in pointermove or rAF.\n    const rect = e.currentTarget.getBoundingClientRect();\n    const cx = rect.left + rect.width / 2;\n    const cy = rect.top + rect.height / 2;\n    if (Math.hypot(e.clientX - cx, e.clientY - cy) > rect.width / 2) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    e.currentTarget.focus({ preventScroll: true });\n    inputSeen.current = true;\n    stopMotion();\n    drag.current = {\n      cx,\n      cy,\n      last: pointerAngle(e.clientX, e.clientY, cx, cy),\n      samples: [{ t: performance.now(), p: posRef.current }],\n    };\n    kick();\n  };\n\n  const onWheelPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d) return;\n    const ang = pointerAngle(e.clientX, e.clientY, d.cx, d.cy);\n    let delta = ang - d.last;\n    if (delta > 180) delta -= 360;\n    else if (delta < -180) delta += 360;\n    d.last = ang;\n    const items = delta / step;\n    const outside = !loop && n > 0 && (posRef.current < 0 || posRef.current > n - 1);\n    posRef.current += outside ? items * RUBBER : items;\n    const t = performance.now();\n    d.samples.push({ t, p: posRef.current });\n    while (d.samples.length > 2 && t - d.samples[0].t > SAMPLE_KEEP_MS) d.samples.shift();\n    paint();\n    reportTo(restStep(Math.round(posRef.current), n, loop));\n  };\n\n  const onWheelPointerEnd = (e: PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d) return;\n    drag.current = null;\n    if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    }\n    const t = performance.now();\n    d.samples.push({ t, p: posRef.current });\n    const recent = d.samples.filter((s) => t - s.t <= RELEASE_WINDOW_MS);\n    let v = 0;\n    if (recent.length >= 2) {\n      const a = recent[0];\n      const b = recent[recent.length - 1];\n      const span = (b.t - a.t) / 1000;\n      if (span > 0) v = (b.p - a.p) / span;\n    }\n    v = clamp(v, -MAX_FLICK, MAX_FLICK);\n    const pos = posRef.current;\n    const outside = !loop && n > 0 && (pos < 0 || pos > n - 1);\n    const target = restStep(Math.round(pos), n, loop);\n    if (reduced) {\n      velRef.current = 0;\n      posRef.current = target;\n      paint();\n      reportTo(target);\n    } else if (!outside && Math.abs(v) >= SNAP_SPEED) {\n      velRef.current = v;\n      springTarget.current = null;\n    } else {\n      velRef.current = 0;\n      startSpring(target, 0);\n    }\n    kick();\n  };\n\n  const onWheelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (n === 0) return;\n    const cur = lastStepRef.current;\n    let target: number;\n    switch (e.key) {\n      case \"ArrowUp\":\n      case \"ArrowLeft\":\n        target = cur - 1;\n        break;\n      case \"ArrowDown\":\n      case \"ArrowRight\":\n        target = cur + 1;\n        break;\n      case \"PageUp\":\n        target = cur - 5;\n        break;\n      case \"PageDown\":\n        target = cur + 5;\n        break;\n      case \"Home\":\n        target = loop ? cur - mod(cur, n) : 0;\n        break;\n      case \"End\":\n        target = loop ? cur + (n - 1 - mod(cur, n)) : n - 1;\n        break;\n      case \"Enter\":\n      case \" \":\n        e.preventDefault();\n        selectCurrent();\n        return;\n      default:\n        return;\n    }\n    e.preventDefault();\n    inputSeen.current = true;\n    const dest = restStep(target, n, loop);\n    stopMotion();\n    if (dest === cur) return;\n    if (reduced) {\n      posRef.current = dest;\n    } else {\n      tween.current = { from: posRef.current, to: dest, t0: performance.now() };\n    }\n    reportTo(dest);\n  };\n\n  const shown = n === 0 ? 0 : wrapIndex(isControlled ? value : index, n, loop);\n  const current = n === 0 ? undefined : items[shown];\n\n  const surface = tok(\"surface\", p.surface);\n  const border = tok(\"border\", p.border);\n  const borderStrong = tok(\"border-strong\", p.borderStrong);\n  const text = tok(\"text\", p.text);\n  const faint = tok(\"text-faint\", p.textFaint);\n  const muted = tok(\"text-muted\", p.textMuted);\n  const soft = tok(\"text-soft\", p.textSoft);\n  const medium = tok(\"text-medium\", p.textMedium);\n  const accent = tok(\"accent\", p.accent);\n  const faceCentre = dark ? p.raised : p.surface;\n  const faceEdge = dark ? \"#0d0d0d\" : p.raised;\n\n  const renderRows = (inverted: boolean) =>\n    Array.from({ length: rowCount }, (_, k) => {\n      const idx = k % n;\n      const copy = Math.floor(k / n);\n      const isMid = copy === mid;\n      const item = items[idx];\n      return (\n        <WheelRow\n          key={`${copy}-${idx}`}\n          id={!inverted && isMid ? optionId(idx) : undefined}\n          option={!inverted && isMid}\n          selected={idx === shown}\n          hidden={!isMid}\n          label={item.label}\n          meta={item.meta}\n          labelColor={inverted ? tok(\"bg\", p.bg) : medium}\n          metaColor={inverted ? tok(\"bg\", p.bg) : soft}\n        />\n      );\n    });\n\n  const display = stacked && (\n    <div\n      role=\"listbox\"\n      id={listboxId}\n      aria-label={`${ariaLabel} items`}\n      aria-activedescendant={n > 0 ? optionId(shown) : undefined}\n      className=\"relative overflow-hidden\"\n      style={{\n        width: LIST_W,\n        height: LIST_H,\n        borderRadius: 16,\n        background: surface,\n        border: `1px solid ${border}`,\n        boxShadow: tok(\"shadow-inset\", \"inset 0 1px 10px rgba(88,72,49,0.12)\"),\n      }}\n    >\n      <div className=\"absolute inset-0\" style={{ WebkitMaskImage: FADE_MASK, maskImage: FADE_MASK }}>\n        <div ref={listEl} className=\"absolute inset-x-0 top-0 will-change-transform\">\n          {renderRows(false)}\n        </div>\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute rounded-[8px]\"\n          style={{ left: PILL_INSET, right: PILL_INSET, top: ROW_TOP, height: ROW_H, background: text }}\n        />\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n          style={{\n            clipPath: `inset(${ROW_TOP}px ${PILL_INSET}px ${LIST_H - ROW_TOP - ROW_H}px ${PILL_INSET}px round ${PILL_RADIUS}px)`,\n          }}\n        >\n          <div ref={invEl} className=\"absolute inset-x-0 top-0 will-change-transform\">\n            {renderRows(true)}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n\n  return (\n    <div\n      ref={rootEl}\n      className={cn(\"relative flex flex-col items-center\", className)}\n      style={{\n        width: stacked ? LIST_W : size,\n        gap: stacked ? STACK_GAP : 0,\n      }}\n    >\n      {display}\n      <div className=\"relative shrink-0\" style={{ width: size, height: size }}>\n        <div\n          ref={wheelEl}\n          role=\"slider\"\n          tabIndex={n > 0 ? 0 : -1}\n          aria-label={ariaLabel}\n          aria-valuemin={0}\n          aria-valuemax={Math.max(0, n - 1)}\n          aria-valuenow={shown}\n          aria-valuetext={current ? current.label : \"No items\"}\n          aria-controls={stacked ? listboxId : undefined}\n          aria-disabled={n === 0 || undefined}\n          onPointerDown={onWheelPointerDown}\n          onPointerMove={onWheelPointerMove}\n          onPointerUp={onWheelPointerEnd}\n          onPointerCancel={onWheelPointerEnd}\n          onKeyDown={onWheelKeyDown}\n          className=\"absolute inset-0 touch-none select-none rounded-full outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)] aria-disabled:opacity-45\"\n          style={{\n            background: `radial-gradient(circle at 50% 50%, ${faceCentre} 0%, ${faceEdge} 100%)`,\n            border: `1px solid ${border}`,\n          }}\n        >\n          {dark && (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 rounded-full\"\n              style={{ boxShadow: \"inset 0 1px 0 rgba(255,255,255,0.05)\" }}\n            />\n          )}\n          {Array.from({ length: detents }, (_, i) => (\n            <span\n              key={i}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0\"\n              style={{ transform: `rotate(${i * step}deg)` }}\n            >\n              <span\n                ref={(el) => {\n                  tickEls.current[i] = el;\n                }}\n                className=\"absolute h-[6px] w-px\"\n                style={{\n                  left: \"calc(50% - 0.5px)\",\n                  top: 7,\n                  transformOrigin: \"50% 0\",\n                  background: n > 0 && i === shown % detents ? accent : resolvedTone === \"light\" ? soft : faint,\n                }}\n              />\n            </span>\n          ))}\n          <span ref={notchEl} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n            <span\n              className=\"absolute h-1 w-1 rounded-full\"\n              style={{ left: \"calc(50% - 2px)\", top: 20, background: muted }}\n            />\n          </span>\n        </div>\n        {/* pl offsets the trailing letter-spacing so the ink is centred (blur test: -0.44px) */}\n        <motion.button\n          type=\"button\"\n          aria-label={current ? `Select ${current.label}` : \"Select\"}\n          disabled={n === 0}\n          onClick={selectCurrent}\n          whileTap={{ scale: 0.96 }}\n          transition={springs.press}\n          className=\"absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full pl-[0.08em] font-mono text-[11px] uppercase leading-none tracking-[0.08em] outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)] disabled:opacity-45\"\n          style={{\n            width: centerD,\n            height: centerD,\n            background: surface,\n            border: `1px solid ${borderStrong}`,\n            color: text,\n          }}\n        >\n          {centerLabel}\n        </motion.button>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/controls/click-wheel.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/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/motion.ts",
      "content": "// Shared springs, easings and durations. Components import these instead of inventing values.\n\nexport const springs = {\n  press: { type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 },\n  snappy: { type: \"spring\", stiffness: 400, damping: 25, mass: 1 },\n  standard: { type: \"spring\", stiffness: 300, damping: 30, mass: 0.8 },\n  soft: { type: \"spring\", stiffness: 200, damping: 25, mass: 1.2 },\n  settle: { type: \"spring\", stiffness: 380, damping: 30, mass: 0.8 },\n  blurIn: { type: \"spring\", stiffness: 400, damping: 28, mass: 0.6 },\n} as const;\n\nexport const ease = {\n  out: [0.23, 1, 0.32, 1],\n  inOut: [0.77, 0, 0.175, 1],\n  drawer: [0.32, 0.72, 0, 1],\n} as const;\n\nexport const easeCss = {\n  out: \"cubic-bezier(0.23,1,0.32,1)\",\n  inOut: \"cubic-bezier(0.77,0,0.175,1)\",\n  drawer: \"cubic-bezier(0.32,0.72,0,1)\",\n} as const;\n\nexport const durations = { press: 140, tooltip: 160, menu: 200, panel: 280 } as const; // ms\n\n// Cubic bezier easing for canvas and rAF code. Returns progress y for input x in [0, 1].\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (s: number) => ((ax * s + bx) * s + cx) * s;\n  const sampleY = (s: number) => ((ay * s + by) * s + cy) * s;\n  const sampleDX = (s: number) => (3 * ax * s + 2 * bx) * s + cx;\n\n  const solveX = (x: number) => {\n    let s = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(s) - x;\n      if (Math.abs(err) < 1e-6) return s;\n      const d = sampleDX(s);\n      if (Math.abs(d) < 1e-6) break;\n      s -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    s = x;\n    while (lo < hi) {\n      const v = sampleX(s);\n      if (Math.abs(v - x) < 1e-6) return s;\n      if (x > v) lo = s;\n      else hi = s;\n      s = (lo + hi) / 2;\n      if (hi - lo < 1e-7) break;\n    }\n    return s;\n  };\n\n  return (t: number) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    return sampleY(solveX(t));\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/motion.ts"
    },
    {
      "path": "components/bjork-ui/_core/spring.ts",
      "content": "// Pure spring math, no React. Units: stiffness k, damping d (coefficient), mass m, time in seconds.\n\nexport interface SpringConfig {\n  stiffness: number;\n  damping: number;\n  mass: number;\n}\n\nexport interface SpringState {\n  x: number;\n  v: number;\n}\n\n// Damping ratio zeta = d / (2 * sqrt(k * m)).\nexport function dampingRatio(c: SpringConfig): number {\n  return c.damping / (2 * Math.sqrt(c.stiffness * c.mass));\n}\n\n// Exact solution of y'' = -(k/m) y - (d/m) y' for displacement y0 and velocity v0 at time t.\n// Returns displacement and velocity together so stepping can be exact, not integrated.\nfunction solveLinear(t: number, c: SpringConfig, y0: number, v0: number): { y: number; v: number } {\n  const w0 = Math.sqrt(c.stiffness / c.mass);\n  const z = dampingRatio(c);\n\n  if (Math.abs(z - 1) < 1e-9) {\n    // Critically damped.\n    const e = Math.exp(-w0 * t);\n    const b = v0 + w0 * y0;\n    return { y: e * (y0 + b * t), v: e * (b - w0 * (y0 + b * t)) };\n  }\n  if (z < 1) {\n    // Underdamped.\n    const wd = w0 * Math.sqrt(1 - z * z);\n    const a = z * w0;\n    const B = (v0 + a * y0) / wd;\n    const e = Math.exp(-a * t);\n    const cos = Math.cos(wd * t);\n    const sin = Math.sin(wd * t);\n    return {\n      y: e * (y0 * cos + B * sin),\n      v: e * (-a * (y0 * cos + B * sin) + wd * (-y0 * sin + B * cos)),\n    };\n  }\n  // Overdamped.\n  const s = w0 * Math.sqrt(z * z - 1);\n  const r1 = -z * w0 + s;\n  const r2 = -z * w0 - s;\n  const B = (v0 - r1 * y0) / (r2 - r1);\n  const A = y0 - B;\n  const e1 = Math.exp(r1 * t);\n  const e2 = Math.exp(r2 * t);\n  return { y: A * e1 + B * e2, v: A * r1 * e1 + B * r2 * e2 };\n}\n\n// Analytic position at time t for a spring from `from` to `to` with initial velocity v0.\nexport function springAt(t: number, c: SpringConfig, from = 0, to = 1, v0 = 0): number {\n  return to + solveLinear(t, c, from - to, v0).y;\n}\n\n// Overshoot as a fraction of the total travel. Zero when critically or overdamped.\nexport function overshoot(c: SpringConfig): number {\n  const z = dampingRatio(c);\n  if (z >= 1) return 0;\n  return Math.exp((-Math.PI * z) / Math.sqrt(1 - z * z));\n}\n\n// Seconds until the spring stays within epsilon of its target (fraction of travel).\n// Found by scanning backwards from a bound derived from the slowest decay rate.\nexport function settleTime(c: SpringConfig, epsilon = 0.001): number {\n  const w0 = Math.sqrt(c.stiffness / c.mass);\n  const z = dampingRatio(c);\n  const slowest = z < 1 ? z * w0 : (z - Math.sqrt(z * z - 1)) * w0;\n  const tEnd = (Math.log(1 / epsilon) / slowest) * 1.5 + 0.05;\n  const steps = 4000;\n  const dt = tEnd / steps;\n  const band = epsilon;\n  let last = 0;\n  for (let i = 0; i <= steps; i++) {\n    const t = i * dt;\n    if (Math.abs(springAt(t, c, 1, 0, 0)) >= band) last = t;\n  }\n  return last + dt;\n}\n\n// Advances the state by dt towards `target`. Uses the exact solution of the linear spring\n// (no integration), so it tracks springAt to rounding error at any step size and stays stable\n// for stiff configs where a fixed-step integrator drifts.\nexport function stepSpring(s: SpringState, target: number, c: SpringConfig, dt: number): void {\n  if (dt <= 0) return;\n  const r = solveLinear(dt, c, s.x - target, s.v);\n  s.x = target + r.y;\n  s.v = r.v;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/spring.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"
    }
  ],
  "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": "Controls",
    "registryUrl": "https://ui.isaiahbjork.com/click-wheel.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/click-wheel.json"
  },
  "type": "registry:component"
}