{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glow-rules",
  "title": "Glow rules",
  "description": "Glow rules is a production-ready utilities component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/misc/glow-rules.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  type CSSProperties,\n  type HTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport {\n  BJORK_PALETTE,\n  type BjorkTone,\n} from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { cn } from \"@/lib/utils\";\n\nexport type GlowRulesAxis = \"rows\" | \"cols\" | \"grid\";\n\nexport interface GlowRulesTether {\n  /** Centre of the second light, in px from the container's top-left. */\n  x: number;\n  y: number;\n  /** Half-width of the light in px. Default 360. */\n  width?: number;\n  /** Half-height of the light in px. Default 56. */\n  height?: number;\n  /** 0 to 1. Default 1. */\n  intensity?: number;\n}\n\nexport interface GlowRulesProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"color\"\n> {\n  children?: ReactNode;\n  /** Elements whose edges become rules. Matched inside the container. Default `\"li\"`. */\n  selector?: string;\n  /** `rows` draws horizontal rules, `cols` vertical rules, `grid` both. Default `\"rows\"`. */\n  axis?: GlowRulesAxis;\n  /** Light size: [half-width as a share of the container, half-height in px]. Default `[0.4, 100]`. */\n  radius?: [number, number];\n  /** Share of the remaining distance the light covers per 60 Hz frame. Default 0.12. */\n  lerp?: number;\n  /** Snaps the light to this grid (px) so style writes stay rare. Default 4. */\n  quantize?: number;\n  /** Light colour. Defaults to white on dark and warm ink on light. */\n  color?: string;\n  /** Peak opacity of the light, 0 to 1. Default 1 on dark, 0.85 on light. */\n  intensity?: number;\n  /** Colour of the resting rules. `false` hides them, so the light only shows where you already draw borders. */\n  ruleColor?: string | false;\n  /** Blend for the light. `auto` uses screen on dark and multiply on light. */\n  blend?: \"auto\" | CSSProperties[\"mixBlendMode\"];\n  /** Draw a rule above the first item and below the last. Default true. */\n  outerEdges?: boolean;\n  /** Keyboard focus inside a matched element moves the light there. Default true. */\n  followFocus?: boolean;\n  /** A second light pinned to a point, e.g. to lead the eye toward a card. */\n  tether?: GlowRulesTether | null;\n  /** Draws the rules in from the left the first time they are measured. Default false. */\n  drawIn?: boolean;\n  /** Turns the light off without unmounting. */\n  disabled?: boolean;\n  tone?: BjorkTone;\n}\n\nexport interface GlowRulesHandle {\n  /** Re-measures the rules. Call after a change the observers cannot see, such as a transform-free reflow. */\n  measure: () => void;\n  /** Moves the light to a point (container px) and shows it, as keyboard focus does. */\n  pointAt: (x: number, y: number) => void;\n  /** Hides the light. */\n  release: () => void;\n  element: HTMLDivElement | null;\n}\n\nconst FADE_MS = 350;\n\nfunction offsetWithin(el: HTMLElement, root: HTMLElement) {\n  // offsetTop/Left ignore transforms, so entrance animations on the items do not drag the rules around.\n  let x = 0;\n  let y = 0;\n  let node: HTMLElement | null = el;\n  while (node && node !== root) {\n    x += node.offsetLeft;\n    y += node.offsetTop;\n    const parent = node.offsetParent as HTMLElement | null;\n    if (!parent || !root.contains(parent)) {\n      if (parent !== root) {\n        // root is not an offsetParent of this chain: fall back to layout rects.\n        const a = el.getBoundingClientRect();\n        const b = root.getBoundingClientRect();\n        return {\n          x: a.left - b.left,\n          y: a.top - b.top,\n          w: el.offsetWidth,\n          h: el.offsetHeight,\n        };\n      }\n    }\n    node = parent;\n  }\n  return { x, y, w: el.offsetWidth, h: el.offsetHeight };\n}\n\nfunction prefersReducedMotion() {\n  return (\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\n/**\n * Hairline rules that catch a soft light under the pointer. One overlay draws every rule:\n * the rules are 1 px mask layers measured from the matched children, the light is a single\n * radial gradient behind that mask. The pointer is eased and quantised; the loop sleeps when settled.\n */\nexport const GlowRules = forwardRef<GlowRulesHandle, GlowRulesProps>(\n  function GlowRules(\n    {\n      children,\n      selector = \"li\",\n      axis = \"rows\",\n      radius = [0.4, 100],\n      lerp = 0.12,\n      quantize = 4,\n      color,\n      intensity,\n      ruleColor,\n      blend = \"auto\",\n      outerEdges = true,\n      followFocus = true,\n      tether = null,\n      drawIn = false,\n      disabled = false,\n      tone,\n      className,\n      style,\n      onPointerMove,\n      onPointerEnter,\n      onPointerLeave,\n      onFocus,\n      onBlur,\n      ...rest\n    },\n    ref,\n  ) {\n    const resolved = useBjorkTone(tone);\n    const palette = BJORK_PALETTE[resolved];\n    const isDark = resolved === \"dark\";\n    const lightColor = color ?? (isDark ? \"#ffffff\" : \"#6b4a2b\");\n    const peak = intensity ?? (isDark ? 1 : 0.85);\n    const rules = ruleColor === undefined ? palette.hair : ruleColor;\n    const mixBlend =\n      blend === \"auto\" ? (isDark ? \"screen\" : \"multiply\") : blend;\n\n    const rootRef = useRef<HTMLDivElement>(null);\n    const layersRef = useRef<HTMLDivElement>(null);\n    const glowRef = useRef<HTMLDivElement>(null);\n    const tetherRef = useRef<HTMLDivElement>(null);\n    const drawnRef = useRef(false);\n\n    // Pointer state lives in refs; React never re-renders for it.\n    const state = useRef({\n      tx: -9999,\n      ty: -9999,\n      x: -9999,\n      y: -9999,\n      lastX: NaN,\n      lastY: NaN,\n      raf: 0,\n      last: 0,\n      pointerInside: false,\n      focusInside: false,\n      snapNext: true,\n      // Tether light: eased toward its target in the same loop.\n      hx: NaN,\n      hy: NaN,\n      htx: NaN,\n      hty: NaN,\n      hLastX: NaN,\n      hLastY: NaN,\n    });\n    const optsRef = useRef({ lerp, quantize });\n    useEffect(() => {\n      optsRef.current = { lerp, quantize };\n    }, [lerp, quantize]);\n\n    const write = useCallback((x: number, y: number) => {\n      const el = glowRef.current;\n      if (!el) return;\n      el.style.setProperty(\"--gr-x\", `${x}px`);\n      el.style.setProperty(\"--gr-y\", `${y}px`);\n    }, []);\n\n    const tick = useCallback(\n      (now: number) => {\n        const s = state.current;\n        s.raf = 0;\n        const dt = s.last ? Math.min(64, now - s.last) : 16.7;\n        s.last = now;\n        const reduce = prefersReducedMotion();\n        const { lerp: l, quantize: q } = optsRef.current;\n        const k =\n          s.snapNext || reduce\n            ? 1\n            : 1 - Math.pow(1 - Math.min(1, Math.max(0.001, l)), dt / 16.667);\n        s.snapNext = false;\n        s.x += (s.tx - s.x) * k;\n        s.y += (s.ty - s.y) * k;\n        const step = Math.max(1, q);\n        const qx = Math.round(s.x / step) * step;\n        const qy = Math.round(s.y / step) * step;\n        if (qx !== s.lastX || qy !== s.lastY) {\n          write(qx, qy);\n          s.lastX = qx;\n          s.lastY = qy;\n        }\n        let tetherMoving = false;\n        if (!Number.isNaN(s.htx)) {\n          if (Number.isNaN(s.hx) || reduce) {\n            s.hx = s.htx;\n            s.hy = s.hty;\n          } else {\n            // A touch slower than the pointer light, so the tether trails it.\n            const kt = 1 - Math.pow(1 - 0.16, dt / 16.667);\n            s.hx += (s.htx - s.hx) * kt;\n            s.hy += (s.hty - s.hy) * kt;\n          }\n          const hx = Math.round(s.hx);\n          const hy = Math.round(s.hy);\n          if (hx !== s.hLastX || hy !== s.hLastY) {\n            tetherRef.current?.style.setProperty(\"--gt-x\", `${hx}px`);\n            tetherRef.current?.style.setProperty(\"--gt-y\", `${hy}px`);\n            s.hLastX = hx;\n            s.hLastY = hy;\n          }\n          tetherMoving =\n            Math.abs(s.htx - s.hx) > 0.5 || Math.abs(s.hty - s.hy) > 0.5;\n        }\n        if (\n          tetherMoving ||\n          Math.abs(s.tx - s.x) > 0.5 ||\n          Math.abs(s.ty - s.y) > 0.5\n        ) {\n          s.raf = requestAnimationFrame(tick);\n        } else {\n          s.last = 0;\n        }\n      },\n      [write],\n    );\n\n    const kick = useCallback(() => {\n      const s = state.current;\n      if (!s.raf) s.raf = requestAnimationFrame(tick);\n    }, [tick]);\n\n    const setOn = useCallback(\n      (on: boolean) => {\n        const el = glowRef.current;\n        if (!el) return;\n        el.style.opacity = on && !disabled ? String(peak) : \"0\";\n      },\n      [disabled, peak],\n    );\n\n    const measure = useCallback(() => {\n      const root = rootRef.current;\n      const layers = layersRef.current;\n      if (!root || !layers) return;\n      let items: HTMLElement[] = [];\n      try {\n        items = Array.from(root.querySelectorAll<HTMLElement>(selector)).filter(\n          (el) => el.offsetParent !== null || el.getClientRects().length > 0,\n        );\n      } catch {\n        items = [];\n      }\n      const images: string[] = [];\n      const sizes: string[] = [];\n      const positions: string[] = [];\n      const push = (x: number, y: number, w: number, h: number) => {\n        images.push(\"linear-gradient(#000,#000)\");\n        sizes.push(`${Math.round(w)}px ${Math.round(h)}px`);\n        positions.push(`${Math.round(x)}px ${Math.round(y)}px`);\n      };\n      const boxes = items.map((el) => offsetWithin(el, root));\n      const near = (a: number, b: number) => Math.abs(a - b) <= 1.5;\n      const overlapX = (a: (typeof boxes)[number], b: (typeof boxes)[number]) =>\n        a.x < b.x + b.w - 1 && b.x < a.x + a.w - 1;\n      const overlapY = (a: (typeof boxes)[number], b: (typeof boxes)[number]) =>\n        a.y < b.y + b.h - 1 && b.y < a.y + a.h - 1;\n      const seen = new Set<string>();\n      const line = (x: number, y: number, w: number, h: number) => {\n        const key = `${Math.round(x)}:${Math.round(y)}:${Math.round(w)}:${Math.round(h)}`;\n        if (seen.has(key)) return;\n        seen.add(key);\n        push(x, y, w, h);\n      };\n      if (axis === \"rows\" || axis === \"grid\") {\n        boxes.forEach((b) => {\n          // Top edge, unless it is the outer edge and outer edges are off.\n          const hasAbove = boxes.some(\n            (o) => o !== b && near(o.y + o.h, b.y) && overlapX(o, b),\n          );\n          if (outerEdges || hasAbove) line(b.x, b.y, b.w, 1);\n          // Bottom edge only where nothing sits directly below, so shared edges draw once.\n          const hasBelow = boxes.some(\n            (o) => o !== b && near(o.y, b.y + b.h) && overlapX(o, b),\n          );\n          if (outerEdges && !hasBelow) line(b.x, b.y + b.h - 1, b.w, 1);\n        });\n      }\n      if (axis === \"cols\" || axis === \"grid\") {\n        boxes.forEach((b) => {\n          const hasLeft = boxes.some(\n            (o) => o !== b && near(o.x + o.w, b.x) && overlapY(o, b),\n          );\n          if (outerEdges || hasLeft) line(b.x, b.y, 1, b.h);\n          const hasRight = boxes.some(\n            (o) => o !== b && near(o.x, b.x + b.w) && overlapY(o, b),\n          );\n          if (outerEdges && !hasRight) line(b.x + b.w - 1, b.y, 1, b.h);\n        });\n      }\n      const mask = images.length\n        ? images.join(\",\")\n        : \"linear-gradient(transparent,transparent)\";\n      layers.style.setProperty(\"--gr-mask\", mask);\n      layers.style.setProperty(\"--gr-mask-size\", sizes.join(\",\") || \"0 0\");\n      layers.style.setProperty(\"--gr-mask-pos\", positions.join(\",\") || \"0 0\");\n      if (drawIn && !drawnRef.current && images.length) {\n        drawnRef.current = true;\n        if (!prefersReducedMotion()) {\n          layers.animate(\n            [\n              { clipPath: \"inset(0 100% 0 0)\" },\n              { clipPath: \"inset(0 0% 0 0)\" },\n            ],\n            {\n              duration: 700,\n              easing: \"cubic-bezier(0.23,1,0.32,1)\",\n              fill: \"both\",\n            },\n          );\n        }\n      }\n    }, [selector, axis, outerEdges, drawIn]);\n\n    // Measure on size and structure changes. Observing every matched item catches rows that grow.\n    useEffect(() => {\n      const root = rootRef.current;\n      if (!root) return;\n      let frame = 0;\n      const schedule = () => {\n        if (frame) return;\n        frame = requestAnimationFrame(() => {\n          frame = 0;\n          measure();\n          observeItems();\n        });\n      };\n      const ro = new ResizeObserver(schedule);\n      const observed = new Set<Element>();\n      const observeItems = () => {\n        let items: Element[] = [];\n        try {\n          items = Array.from(root.querySelectorAll(selector));\n        } catch {\n          items = [];\n        }\n        for (const el of items) {\n          if (!observed.has(el)) {\n            observed.add(el);\n            ro.observe(el);\n          }\n        }\n      };\n      ro.observe(root);\n      observeItems();\n      const mo = new MutationObserver(schedule);\n      mo.observe(root, { childList: true, subtree: true });\n      schedule();\n      if (typeof document !== \"undefined\" && document.fonts?.ready) {\n        document.fonts.ready.then(schedule).catch(() => {});\n      }\n      return () => {\n        if (frame) cancelAnimationFrame(frame);\n        ro.disconnect();\n        mo.disconnect();\n      };\n    }, [measure, selector]);\n\n    useEffect(() => {\n      const s = state.current;\n      return () => {\n        if (s.raf) cancelAnimationFrame(s.raf);\n      };\n    }, []);\n\n    useEffect(() => {\n      setOn(state.current.pointerInside || state.current.focusInside);\n    }, [setOn]);\n\n    // Tether: keep the last position while it fades out, ease to new positions while it is on.\n    const tetherX = tether?.x;\n    const tetherY = tether?.y;\n    const tetherOn = tether != null && !disabled;\n    const tetherAlpha = (tether?.intensity ?? 1) * peak;\n    useEffect(() => {\n      const el = tetherRef.current;\n      if (!el) return;\n      const s = state.current;\n      if (tetherOn && tetherX != null && tetherY != null) {\n        const wasOff = el.style.opacity === \"0\" || el.style.opacity === \"\";\n        s.htx = tetherX;\n        s.hty = tetherY;\n        if (wasOff) {\n          // Appearing: start in place rather than sliding from the last row.\n          s.hx = NaN;\n          s.hy = NaN;\n        }\n        el.style.opacity = String(tetherAlpha);\n        kick();\n      } else {\n        el.style.opacity = \"0\";\n      }\n    }, [tetherOn, tetherX, tetherY, tetherAlpha, kick]);\n\n    const pointAt = useCallback(\n      (x: number, y: number) => {\n        const s = state.current;\n        const wasOff = !(s.pointerInside || s.focusInside);\n        s.tx = x;\n        s.ty = y;\n        if (wasOff && s.x < -9000) s.snapNext = true;\n        s.focusInside = true;\n        setOn(true);\n        kick();\n      },\n      [kick, setOn],\n    );\n\n    const release = useCallback(() => {\n      state.current.focusInside = false;\n      setOn(state.current.pointerInside);\n    }, [setOn]);\n\n    useImperativeHandle(\n      ref,\n      () => ({ measure, pointAt, release, element: rootRef.current }),\n      [measure, pointAt, release],\n    );\n\n    // Works inside scaled parents: layout px = screen px / scale.\n    const toLocal = (clientX: number, clientY: number) => {\n      const el = rootRef.current!;\n      const r = el.getBoundingClientRect();\n      const sx = el.offsetWidth ? r.width / el.offsetWidth : 1;\n      const sy = el.offsetHeight ? r.height / el.offsetHeight : 1;\n      return {\n        x: (clientX - r.left) / (sx || 1),\n        y: (clientY - r.top) / (sy || 1),\n      };\n    };\n\n    const [rx, ry] = radius;\n    const glowBg = `radial-gradient(ellipse ${Math.max(1, rx * 100)}% ${ry}px at var(--gr-x, -9999px) var(--gr-y, -9999px), ${lightColor} 0%, color-mix(in srgb, ${lightColor} 55%, transparent) 38%, transparent 100%)`;\n    const tetherBg = `radial-gradient(ellipse ${tether?.width ?? 360}px ${tether?.height ?? 56}px at var(--gt-x, -9999px) var(--gt-y, -9999px), ${lightColor} 0%, color-mix(in srgb, ${lightColor} 40%, transparent) 45%, transparent 100%)`;\n\n    const maskStyle: CSSProperties = {\n      WebkitMaskImage: \"var(--gr-mask)\",\n      maskImage: \"var(--gr-mask)\",\n      WebkitMaskSize: \"var(--gr-mask-size)\",\n      maskSize: \"var(--gr-mask-size)\",\n      WebkitMaskPosition: \"var(--gr-mask-pos)\",\n      maskPosition: \"var(--gr-mask-pos)\",\n      WebkitMaskRepeat: \"no-repeat\",\n      maskRepeat: \"no-repeat\",\n    };\n\n    return (\n      <div\n        ref={rootRef}\n        className={cn(\"relative\", className)}\n        style={style}\n        data-glow-rules=\"\"\n        onPointerMove={(e) => {\n          onPointerMove?.(e);\n          if (disabled || e.pointerType === \"touch\") return;\n          const s = state.current;\n          const p = toLocal(e.clientX, e.clientY);\n          s.tx = p.x;\n          s.ty = p.y;\n          if (!s.pointerInside) {\n            s.pointerInside = true;\n            // First contact: start the light under the pointer instead of sweeping in from the last spot.\n            if (!s.focusInside) s.snapNext = true;\n            setOn(true);\n          }\n          kick();\n        }}\n        onPointerEnter={(e) => {\n          onPointerEnter?.(e);\n          if (disabled || e.pointerType === \"touch\") return;\n          const s = state.current;\n          const p = toLocal(e.clientX, e.clientY);\n          s.tx = p.x;\n          s.ty = p.y;\n          s.pointerInside = true;\n          if (!s.focusInside) s.snapNext = true;\n          setOn(true);\n          kick();\n        }}\n        onPointerLeave={(e) => {\n          onPointerLeave?.(e);\n          state.current.pointerInside = false;\n          setOn(state.current.focusInside);\n        }}\n        onFocus={(e) => {\n          onFocus?.(e);\n          if (!followFocus || disabled) return;\n          const target = e.target as HTMLElement;\n          let item: HTMLElement | null = null;\n          try {\n            item = target.closest<HTMLElement>(selector);\n          } catch {\n            item = null;\n          }\n          if (!item || !rootRef.current?.contains(item)) return;\n          if (!target.matches(\":focus-visible\")) return;\n          const b = offsetWithin(item, rootRef.current);\n          // Light the left third of the item, where the eye starts reading.\n          pointAt(b.x + Math.min(b.w * 0.3, 320), b.y + b.h / 2);\n        }}\n        onBlur={(e) => {\n          onBlur?.(e);\n          const next = e.relatedTarget as Node | null;\n          if (next && rootRef.current?.contains(next)) return;\n          release();\n        }}\n        {...rest}\n      >\n        {children}\n        <div\n          ref={layersRef}\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0\"\n          style={{ zIndex: 1 }}\n        >\n          {rules !== false && (\n            <div\n              className=\"absolute inset-0\"\n              style={{ ...maskStyle, background: rules }}\n            />\n          )}\n          <div\n            ref={glowRef}\n            className=\"absolute inset-0\"\n            style={{\n              ...maskStyle,\n              background: glowBg,\n              mixBlendMode: mixBlend,\n              opacity: 0,\n              transition: `opacity ${FADE_MS}ms ease`,\n            }}\n          />\n          <div\n            ref={tetherRef}\n            className=\"absolute inset-0\"\n            style={{\n              ...maskStyle,\n              background: tetherBg,\n              mixBlendMode: mixBlend,\n              opacity: 0,\n              transition: `opacity ${FADE_MS}ms ease`,\n            }}\n          />\n        </div>\n      </div>\n    );\n  },\n);\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/misc/glow-rules.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": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Utilities",
    "registryUrl": "https://ui.isaiahbjork.com/glow-rules.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/glow-rules.json"
  },
  "type": "registry:component"
}