{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chromatic-text",
  "title": "Chromatic text",
  "description": "Chromatic text is a production-ready text component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/text/chromatic-text.tsx",
      "content": "\"use client\";\n\n// Chromatic Text: crisp, single-coloured type that only splits into colour when it moves.\n// Pointer velocity is splatted into a small flow-field texture that decays (and drifts a little\n// along itself) every frame. The display shader samples the text along that flow with a\n// spectral spread of taps, so fast strokes leave a rainbow smear that settles back to one ink.\n// A soft lens magnifies under the cursor. The loop goes idle as soon as everything has settled.\n\nimport {\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type Ref,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { defaultMaxDpr, sizeCanvas, useElementSize } from \"../_core/canvas\";\nimport { useVisibleLoop } from \"../_core/loop\";\nimport { useBjorkTone } from \"../_core/tone\";\nimport { BJORK_PALETTE, type BjorkTone } from \"../_core/palette\";\n\nexport interface ChromaticTextHandle {\n  /** Sweeps a one-shot aberration across the word. */\n  pulse: () => void;\n}\n\nexport interface ChromaticTextProps {\n  text?: string;\n  /** Ink colour. Defaults to the tone's text colour. */\n  color?: string;\n  /** Background the type usually sits on. Only used to tint the colour fringes correctly. */\n  background?: string;\n  /** Strength of the colour split. 0 to 2, default 1. */\n  intensity?: number;\n  /** Radius of the pointer's influence and lens in CSS px. Defaults to 45% of the font size. */\n  radius?: number;\n  /** Seconds for the aberration to fade after the pointer stops. Default 0.7. */\n  decay?: number;\n  /** Fine grain and scanlines that show up with motion. 0 to 1, default 0. */\n  grain?: number;\n  /** Lens magnification under the cursor. 0 to 0.4, default 0.14. */\n  lens?: number;\n  tone?: BjorkTone;\n  /** Upper bound for the font size in CSS px. The type shrinks to fit narrower containers. */\n  fontSize?: number;\n  fontWeight?: number;\n  fontFamily?: string;\n  /** Run a pulse once the type is ready. */\n  pulseOnMount?: boolean;\n  /** Render a single frame of a pulse frozen mid-sweep. Used for previews. */\n  frozen?: boolean;\n  ariaLabel?: string;\n  className?: string;\n  ref?: Ref<ChromaticTextHandle>;\n}\n\n// ─── WebGL2 support (cached, client only) ────────────────────────────────────\n\nlet webgl2Support: boolean | null = null;\nfunction getWebGL2Support(): boolean {\n  if (webgl2Support === null) {\n    try {\n      const probe = document.createElement(\"canvas\");\n      const ctx = probe.getContext(\"webgl2\");\n      webgl2Support = !!ctx;\n      ctx?.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    } catch {\n      webgl2Support = false;\n    }\n  }\n  return webgl2Support;\n}\n// React StrictMode runs effect cleanups and re-runs them on the same canvas. Losing the context\n// right away would hand the second run a dead context, so the release waits a tick and is\n// cancelled if the canvas is picked up again.\nconst pendingRelease = new WeakMap<HTMLCanvasElement, number>();\nfunction claimCanvas(canvas: HTMLCanvasElement) {\n  const t = pendingRelease.get(canvas);\n  if (t !== undefined) {\n    window.clearTimeout(t);\n    pendingRelease.delete(canvas);\n  }\n}\nfunction releaseCanvas(canvas: HTMLCanvasElement, gl: WebGL2RenderingContext) {\n  pendingRelease.set(\n    canvas,\n    window.setTimeout(() => {\n      pendingRelease.delete(canvas);\n      if (!gl.isContextLost()) gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    }, 0),\n  );\n}\nconst subscribeNoop = () => () => {};\nconst getServerSupport = () => true;\n\nlet scratchCtx: CanvasRenderingContext2D | null = null;\nfunction getScratch(): CanvasRenderingContext2D | null {\n  if (!scratchCtx) scratchCtx = document.createElement(\"canvas\").getContext(\"2d\");\n  return scratchCtx;\n}\n\nfunction parseColor(input: string, fallback: [number, number, number]): [number, number, number] {\n  const ctx = getScratch();\n  if (!ctx) return fallback;\n  ctx.fillStyle = \"#000\";\n  ctx.fillStyle = input;\n  const v = String(ctx.fillStyle);\n  if (v.startsWith(\"#\")) {\n    const n = parseInt(v.slice(1, 7), 16);\n    return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n  }\n  const m = v.match(/[\\d.]+/g);\n  if (!m || m.length < 3) return fallback;\n  return [+m[0] / 255, +m[1] / 255, +m[2] / 255];\n}\n\ninterface Layout {\n  width: number;\n  height: number;\n  fontSize: number;\n  font: string;\n  tracking: number;\n  baseline: number;\n  textWidth: number;\n  mid: number;\n}\n\nfunction setTracking(ctx: CanvasRenderingContext2D, px: number) {\n  if (\"letterSpacing\" in ctx) (ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = `${px}px`;\n}\n\nfunction computeLayout(width: number, text: string, family: string, weight: number, maxFont: number): Layout | null {\n  const ctx = getScratch();\n  if (!ctx || width < 8) return null;\n  ctx.font = `${weight} 100px ${family}`;\n  setTracking(ctx, -3);\n  const w100 = Math.max(1, ctx.measureText(text).width);\n  const fontSize = Math.max(14, Math.min(maxFont, (width * 0.86 * 100) / w100));\n  ctx.font = `${weight} ${fontSize}px ${family}`;\n  setTracking(ctx, -0.03 * fontSize);\n  const m = ctx.measureText(text);\n  const asc = m.actualBoundingBoxAscent || fontSize * 0.72;\n  const desc = m.actualBoundingBoxDescent || 0;\n  const height = Math.round(fontSize * 1.32);\n  const baseline = height / 2 + (asc - desc) / 2;\n  return {\n    width,\n    height,\n    fontSize,\n    font: `${weight} ${fontSize}px ${family}`,\n    tracking: -0.03 * fontSize,\n    baseline,\n    textWidth: m.width,\n    mid: baseline - (asc - desc) / 2,\n  };\n}\n\n// ─── Shaders ─────────────────────────────────────────────────────────────────\n\nconst VERT = `#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() { vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`;\n\n// Flow field: decay, self-advect, then splat the pointer stroke (a capsule from A to B).\nconst FLOW_FRAG = `#version 300 es\nprecision highp float;\nin vec2 vUv;\nuniform sampler2D uPrev;\nuniform vec2 uSize;\nuniform vec2 uA;\nuniform vec2 uB;\nuniform vec2 uSplat;\nuniform float uRadius;\nuniform float uDecay;\nuniform float uAdvect;\nuniform float uOn;\nout vec4 outColor;\nvoid main() {\n  vec2 p = vUv * uSize;\n  vec2 here = texture(uPrev, vUv).rg * 2.0 - 1.0;\n  vec2 v = texture(uPrev, vUv - here * uAdvect / uSize).rg * 2.0 - 1.0;\n  v *= uDecay;\n  v = sign(v) * max(abs(v) - 1.5 / 255.0, 0.0);\n  vec2 ab = uB - uA;\n  float t = clamp(dot(p - uA, ab) / max(dot(ab, ab), 1e-4), 0.0, 1.0);\n  float d = length(p - (uA + ab * t));\n  float g = exp(-d * d / (uRadius * uRadius)) * uOn;\n  v += (uSplat - v) * g * 0.85;\n  float l = length(v);\n  if (l > 1.0) v /= l;\n  outColor = vec4(v * 0.5 + 0.5, 0.0, 1.0);\n}`;\n\nconst TAPS = 12;\n\nconst VIEW_FRAG = `#version 300 es\nprecision highp float;\nin vec2 vUv;\nuniform sampler2D uText;\nuniform sampler2D uFlow;\nuniform vec2 uSize;\nuniform float uDpr;\nuniform vec2 uPointer;\nuniform float uHover;\nuniform float uLensR;\nuniform float uLens;\nuniform float uShift;\nuniform float uGrain;\nuniform float uTime;\nuniform vec3 uInk;\nuniform vec3 uBg;\nout vec4 outColor;\n\nvec3 hue(float h) {\n  return clamp(vec3(abs(h * 6.0 - 3.0) - 1.0, 2.0 - abs(h * 6.0 - 2.0), 2.0 - abs(h * 6.0 - 4.0)), 0.0, 1.0);\n}\nfloat hash(vec2 p) {\n  p = fract(p * vec2(123.34, 456.21));\n  p += dot(p, p + 45.32);\n  return fract(p.x * p.y);\n}\n\nvoid main() {\n  vec2 p = gl_FragCoord.xy / uDpr;\n  vec2 d = p - uPointer;\n  float r = length(d) / uLensR;\n  float lens = uHover * uLens * pow(max(0.0, 1.0 - r * r), 2.0);\n  vec2 q = uPointer + d * (1.0 - lens);\n\n  vec2 flow = texture(uFlow, q / uSize).rg * 2.0 - 1.0;\n  flow = sign(flow) * max(abs(flow) - 2.0 / 255.0, 0.0);\n  float speed = length(flow);\n  vec2 off = flow * uShift;\n  vec2 base = q - off * 0.3;\n\n  // Interleaved-gradient jitter turns the discrete taps into one continuous spectral smear.\n  float jit = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5;\n  vec3 acc = vec3(0.0);\n  vec3 wsum = vec3(0.0);\n  for (int i = 0; i < ${TAPS}; i++) {\n    float t = clamp((float(i) + jit * min(1.0, speed * 40.0)) / float(${TAPS - 1}), 0.0, 1.0);\n    vec3 w = hue(t * 0.74) + 0.08;\n    float m = texture(uText, (base + off * (t * 2.0 - 1.0)) / uSize).a;\n    acc += m * w;\n    wsum += w;\n  }\n  vec3 m = acc / wsum;\n\n  if (uGrain > 0.0) {\n    float heat = min(1.0, speed * 2.5 + uHover * 0.1);\n    float n = hash(gl_FragCoord.xy + fract(uTime) * 91.7) - 0.5;\n    float scan = 0.5 + 0.5 * sin(gl_FragCoord.y / uDpr * 3.14159 * 0.75);\n    float g = uGrain * (0.2 + 0.8 * heat);\n    m *= clamp(1.0 + n * g * 0.5 - (1.0 - scan) * g * 0.35, 0.0, 1.2);\n    m = clamp(m, 0.0, 1.0);\n  }\n\n  float a = max(m.r, max(m.g, m.b));\n  // Exact over any background at rest (all channels equal); fringes are tinted for uBg in motion.\n  vec3 col = uInk * m + uBg * (a - m);\n  outColor = vec4(col, a);\n}`;\n\nfunction compile(gl: WebGL2RenderingContext, type: number, src: string): WebGLShader | null {\n  const sh = gl.createShader(type);\n  if (!sh) return null;\n  gl.shaderSource(sh, src);\n  gl.compileShader(sh);\n  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS) && !gl.isContextLost()) {\n    console.warn(\"ChromaticText shader:\", gl.getShaderInfoLog(sh));\n    gl.deleteShader(sh);\n    return null;\n  }\n  return sh;\n}\n\nfunction link(gl: WebGL2RenderingContext, frag: string, names: string[]) {\n  const vs = compile(gl, gl.VERTEX_SHADER, VERT);\n  const fs = compile(gl, gl.FRAGMENT_SHADER, frag);\n  const prog = gl.createProgram();\n  if (!vs || !fs || !prog) return null;\n  gl.attachShader(prog, vs);\n  gl.attachShader(prog, fs);\n  gl.bindAttribLocation(prog, 0, \"aPos\");\n  gl.linkProgram(prog);\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  const u: Record<string, WebGLUniformLocation | null> = {};\n  for (const n of names) u[n] = gl.getUniformLocation(prog, n);\n  return { prog, u };\n}\n\nfunction makeTex(gl: WebGL2RenderingContext) {\n  const tex = gl.createTexture();\n  gl.bindTexture(gl.TEXTURE_2D, tex);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n  return tex;\n}\n\ninterface GLState {\n  gl: WebGL2RenderingContext;\n  view: { prog: WebGLProgram; u: Record<string, WebGLUniformLocation | null> };\n  flow: { prog: WebGLProgram; u: Record<string, WebGLUniformLocation | null> };\n  buf: WebGLBuffer;\n  text: WebGLTexture;\n  fields: { tex: WebGLTexture; fbo: WebGLFramebuffer }[];\n  fieldW: number;\n  fieldH: number;\n  read: number;\n}\n\ninterface Motion {\n  // Pointer in CSS px, y up.\n  px: number;\n  py: number;\n  lastX: number;\n  lastY: number;\n  vx: number;\n  vy: number;\n  hasPointer: boolean;\n  hover: number;\n  hoverTarget: number;\n  energy: number;\n  pulseT: number; // < 0 when no pulse is running\n  pulseLastX: number;\n  time: number;\n  dirty: boolean;\n}\n\nconst PULSE_DURATION = 0.75;\n\nexport function ChromaticText({\n  text = \"Chromatic\",\n  color,\n  background,\n  intensity = 1,\n  radius,\n  decay = 0.7,\n  grain = 0,\n  lens = 0.14,\n  tone,\n  fontSize = 200,\n  fontWeight = 650,\n  fontFamily,\n  pulseOnMount = false,\n  frozen = false,\n  ariaLabel,\n  className,\n  ref,\n}: ChromaticTextProps) {\n  const resolvedTone = useBjorkTone(tone);\n  const reducedMotion = useReducedMotion() ?? false;\n  const supported = useSyncExternalStore(subscribeNoop, getWebGL2Support, getServerSupport);\n  const palette = BJORK_PALETTE[resolvedTone];\n\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const { width } = useElementSize(wrapRef);\n  const [fontsReady, setFontsReady] = useState(false);\n  const [epoch, setEpoch] = useState(0);\n  const [layout, setLayout] = useState<Layout | null>(null);\n\n  const glRef = useRef<GLState | null>(null);\n  const layoutRef = useRef<Layout | null>(null);\n  const dprRef = useRef(1);\n  const motionRef = useRef<Motion>({\n    px: 0,\n    py: 0,\n    lastX: 0,\n    lastY: 0,\n    vx: 0,\n    vy: 0,\n    hasPointer: false,\n    hover: 0,\n    hoverTarget: 0,\n    energy: 0,\n    pulseT: -1,\n    pulseLastX: 0,\n    time: 0,\n    dirty: false,\n  });\n  const look = useRef({\n    ink: [0.93, 0.93, 0.93] as [number, number, number],\n    bg: [0.07, 0.07, 0.07] as [number, number, number],\n    intensity,\n    radius: radius as number | undefined,\n    decay,\n    grain,\n    lens,\n  });\n  const wakeRef = useRef<() => void>(() => {});\n\n  const still = reducedMotion;\n\n  useEffect(() => {\n    let cancelled = false;\n    document.fonts.ready.then(() => {\n      if (!cancelled) setFontsReady(true);\n    });\n    return () => {\n      cancelled = true;\n    };\n  }, []);\n\n  const clearFields = useCallback(() => {\n    const g = glRef.current;\n    if (!g) return;\n    const { gl } = g;\n    for (const f of g.fields) {\n      gl.bindFramebuffer(gl.FRAMEBUFFER, f.fbo);\n      gl.clearColor(0.5, 0.5, 0, 1);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n    }\n    gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n  }, []);\n\n  // Advances the flow field one step, splatting the stroke from A to B (CSS px, y up).\n  const stepField = useCallback(\n    (dt: number, splat: { ax: number; ay: number; bx: number; by: number; vx: number; vy: number; r: number } | null) => {\n      const g = glRef.current;\n      const lay = layoutRef.current;\n      if (!g || !lay) return;\n      const { gl, flow } = g;\n      const L = look.current;\n      const src = g.fields[g.read];\n      const dst = g.fields[1 - g.read];\n      gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);\n      gl.viewport(0, 0, g.fieldW, g.fieldH);\n      gl.useProgram(flow.prog);\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, src.tex);\n      gl.uniform1i(flow.u.uPrev, 0);\n      gl.uniform2f(flow.u.uSize, lay.width, lay.height);\n      const life = Math.max(0.08, L.decay);\n      gl.uniform1f(flow.u.uDecay, Math.exp((-dt * 4.6) / life));\n      gl.uniform1f(flow.u.uAdvect, 140 * dt);\n      if (splat) {\n        gl.uniform2f(flow.u.uA, splat.ax, splat.ay);\n        gl.uniform2f(flow.u.uB, splat.bx, splat.by);\n        gl.uniform2f(flow.u.uSplat, splat.vx, splat.vy);\n        gl.uniform1f(flow.u.uRadius, splat.r);\n        gl.uniform1f(flow.u.uOn, 1);\n      } else {\n        gl.uniform1f(flow.u.uOn, 0);\n        gl.uniform1f(flow.u.uRadius, 1);\n      }\n      gl.bindBuffer(gl.ARRAY_BUFFER, g.buf);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n      g.read = 1 - g.read;\n    },\n    [],\n  );\n\n  const draw = useCallback(() => {\n    const g = glRef.current;\n    const lay = layoutRef.current;\n    const canvas = canvasRef.current;\n    if (!g || !lay || !canvas || g.gl.isContextLost()) return;\n    const { gl, view } = g;\n    const L = look.current;\n    const M = motionRef.current;\n    gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.useProgram(view.prog);\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, g.text);\n    gl.uniform1i(view.u.uText, 0);\n    gl.activeTexture(gl.TEXTURE1);\n    gl.bindTexture(gl.TEXTURE_2D, g.fields[g.read].tex);\n    gl.uniform1i(view.u.uFlow, 1);\n    gl.uniform2f(view.u.uSize, lay.width, lay.height);\n    gl.uniform1f(view.u.uDpr, dprRef.current);\n    gl.uniform2f(view.u.uPointer, M.px, M.py);\n    gl.uniform1f(view.u.uHover, M.hover);\n    gl.uniform1f(view.u.uLensR, L.radius ?? lay.fontSize * 0.45);\n    gl.uniform1f(view.u.uLens, Math.min(0.4, Math.max(0, L.lens)));\n    gl.uniform1f(view.u.uShift, Math.min(2, Math.max(0, L.intensity)) * lay.fontSize * 0.15);\n    gl.uniform1f(view.u.uGrain, Math.min(1, Math.max(0, L.grain)));\n    gl.uniform1f(view.u.uTime, M.time);\n    gl.uniform3f(view.u.uInk, L.ink[0], L.ink[1], L.ink[2]);\n    gl.uniform3f(view.u.uBg, L.bg[0], L.bg[1], L.bg[2]);\n    gl.bindBuffer(gl.ARRAY_BUFFER, g.buf);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  }, []);\n\n  // One simulation step. Returns true while something is still moving.\n  const advance = useCallback(\n    (dt: number): boolean => {\n      const lay = layoutRef.current;\n      if (!lay || !glRef.current) return false;\n      const M = motionRef.current;\n      const L = look.current;\n      const r = L.radius ?? lay.fontSize * 0.45;\n      M.time += dt;\n\n      let splat: Parameters<typeof stepField>[1] = null;\n      if (M.pulseT >= 0) {\n        M.pulseT += dt;\n        const k = Math.min(1, M.pulseT / PULSE_DURATION);\n        const e = k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2;\n        const x0 = lay.width / 2 - lay.textWidth * 0.62;\n        const x1 = lay.width / 2 + lay.textWidth * 0.62;\n        const x = x0 + (x1 - x0) * e;\n        const speed = Math.min(1, Math.abs(x - M.pulseLastX) / Math.max(dt, 1e-3) / 1400);\n        const y = lay.height - lay.mid;\n        splat = { ax: M.pulseLastX, ay: y, bx: x, by: y, vx: speed, vy: speed * 0.18, r: lay.fontSize * 0.55 };\n        M.energy = Math.max(M.energy, speed);\n        M.pulseLastX = x;\n        if (k >= 1) M.pulseT = -1;\n      } else if (M.hasPointer) {\n        const dx = M.px - M.lastX;\n        const dy = M.py - M.lastY;\n        const ivx = dx / Math.max(dt, 1e-3);\n        const ivy = dy / Math.max(dt, 1e-3);\n        M.vx += (ivx - M.vx) * 0.5;\n        M.vy += (ivy - M.vy) * 0.5;\n        // Saturating response: a casual stroke already splits clearly, a flick maxes out.\n        const raw = Math.hypot(M.vx, M.vy);\n        const sl = 1 - Math.exp(-raw / 420);\n        const svx = raw > 1e-3 ? (M.vx / raw) * sl : 0;\n        const svy = raw > 1e-3 ? (M.vy / raw) * sl : 0;\n        if (sl > 0.02) {\n          splat = { ax: M.lastX, ay: M.lastY, bx: M.px, by: M.py, vx: svx, vy: svy, r: r * 0.7 };\n          M.energy = Math.max(M.energy, Math.min(1, sl));\n        }\n        M.lastX = M.px;\n        M.lastY = M.py;\n      }\n      stepField(dt, splat);\n      M.energy *= Math.exp((-dt * 4.6) / Math.max(0.08, L.decay));\n      M.hover += (M.hoverTarget - M.hover) * (1 - Math.exp(-dt * 9));\n      const settled = M.energy < 0.004 && Math.abs(M.hoverTarget - M.hover) < 0.002 && M.pulseT < 0;\n      if (settled) {\n        M.hover = M.hoverTarget;\n        M.energy = 0;\n        M.vx = 0;\n        M.vy = 0;\n        clearFields();\n      }\n      return !settled;\n    },\n    [stepField, clearFields],\n  );\n\n  // Props that only change the look are kept in a ref and repainted in place.\n  useEffect(() => {\n    const L = look.current;\n    L.ink = parseColor(color ?? palette.text, resolvedTone === \"dark\" ? [0.93, 0.93, 0.93] : [0.09, 0.09, 0.09]);\n    L.bg = parseColor(background ?? palette.stage, resolvedTone === \"dark\" ? [0.07, 0.07, 0.07] : [0.97, 0.96, 0.94]);\n    L.intensity = intensity;\n    L.radius = radius;\n    L.decay = decay;\n    L.grain = grain;\n    L.lens = lens;\n    draw();\n  }, [color, background, palette, resolvedTone, intensity, radius, decay, grain, lens, draw]);\n\n  // GL setup / teardown.\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || !supported) return;\n    claimCanvas(canvas);\n    const gl = canvas.getContext(\"webgl2\", { premultipliedAlpha: true, antialias: false, alpha: true });\n    if (!gl) return;\n    const view = link(gl, VIEW_FRAG, [\"uText\", \"uFlow\", \"uSize\", \"uDpr\", \"uPointer\", \"uHover\", \"uLensR\", \"uLens\", \"uShift\", \"uGrain\", \"uTime\", \"uInk\", \"uBg\"]);\n    const flow = link(gl, FLOW_FRAG, [\"uPrev\", \"uSize\", \"uA\", \"uB\", \"uSplat\", \"uRadius\", \"uDecay\", \"uAdvect\", \"uOn\"]);\n    const buf = gl.createBuffer();\n    const textTex = makeTex(gl);\n    if (!view || !flow || !buf || !textTex) return;\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    gl.enableVertexAttribArray(0);\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n    glRef.current = { gl, view, flow, buf, text: textTex, fields: [], fieldW: 0, fieldH: 0, read: 0 };\n\n    const onLost = (e: Event) => {\n      e.preventDefault();\n      glRef.current = null;\n    };\n    const onRestored = () => setEpoch((v) => v + 1);\n    canvas.addEventListener(\"webglcontextlost\", onLost);\n    canvas.addEventListener(\"webglcontextrestored\", onRestored);\n    return () => {\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored);\n      const g = glRef.current;\n      glRef.current = null;\n      if (!gl.isContextLost()) {\n        if (g) {\n          for (const f of g.fields) {\n            gl.deleteFramebuffer(f.fbo);\n            gl.deleteTexture(f.tex);\n          }\n        }\n        gl.deleteTexture(textTex);\n        gl.deleteBuffer(buf);\n        gl.deleteProgram(view.prog);\n        gl.deleteProgram(flow.prog);\n        releaseCanvas(canvas, gl);\n      }\n    };\n  }, [supported, epoch]);\n\n  const { wake } = useVisibleLoop(\n    wrapRef,\n    useCallback(\n      (dt: number) => {\n        const keep = advance(dt);\n        draw();\n        return keep;\n      },\n      [advance, draw],\n    ),\n    { enabled: supported && !still && !frozen },\n  );\n  useEffect(() => {\n    wakeRef.current = wake;\n  }, [wake]);\n\n  // Layout, canvas size, text texture and flow-field targets.\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas || !fontsReady || width < 8) return;\n    let cancelled = false;\n    const family = fontFamily ?? (getComputedStyle(wrap).fontFamily || \"sans-serif\");\n    const run = () => {\n      if (cancelled) return;\n      const lay = computeLayout(width, text, family, fontWeight, fontSize);\n      if (!lay) return;\n      layoutRef.current = lay;\n      setLayout((prev) =>\n        prev && prev.width === lay.width && prev.height === lay.height && prev.fontSize === lay.fontSize ? prev : lay,\n      );\n      const dpr = sizeCanvas(canvas, lay.width, lay.height, defaultMaxDpr());\n      dprRef.current = dpr;\n      const g = glRef.current;\n      if (!g || g.gl.isContextLost()) return;\n      const { gl } = g;\n\n      // Text mask at backing resolution so the resting frame is pixel crisp.\n      const off = document.createElement(\"canvas\");\n      off.width = canvas.width;\n      off.height = canvas.height;\n      const ctx = off.getContext(\"2d\");\n      if (!ctx) return;\n      ctx.scale(dpr, dpr);\n      ctx.font = lay.font;\n      setTracking(ctx, lay.tracking);\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"alphabetic\";\n      ctx.fillStyle = \"#fff\";\n      ctx.fillText(text, lay.width / 2, lay.baseline);\n      gl.bindTexture(gl.TEXTURE_2D, g.text);\n      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, off);\n      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n\n      // Flow field at a quarter of CSS resolution.\n      const fw = Math.max(8, Math.ceil(lay.width / 4));\n      const fh = Math.max(8, Math.ceil(lay.height / 4));\n      if (fw !== g.fieldW || fh !== g.fieldH || g.fields.length === 0) {\n        for (const f of g.fields) {\n          gl.deleteFramebuffer(f.fbo);\n          gl.deleteTexture(f.tex);\n        }\n        g.fields = [];\n        for (let i = 0; i < 2; i++) {\n          const tex = makeTex(gl);\n          const fbo = gl.createFramebuffer();\n          if (!tex || !fbo) return;\n          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, fw, fh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n          gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n          gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);\n          g.fields.push({ tex, fbo });\n        }\n        gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n        g.fieldW = fw;\n        g.fieldH = fh;\n        g.read = 0;\n        clearFields();\n      }\n\n      const M = motionRef.current;\n      if (frozen && !still) {\n        // Replay a pulse offline and stop just past the middle of the sweep.\n        clearFields();\n        M.hover = 0;\n        M.hoverTarget = 0;\n        M.energy = 0;\n        M.pulseT = 0;\n        M.pulseLastX = lay.width / 2 - lay.textWidth * 0.62;\n        const dt = 1 / 60;\n        for (let t = 0; t < PULSE_DURATION * 0.58; t += dt) advance(dt);\n      } else if (pulseOnMount && !still && M.pulseT < 0 && M.time === 0) {\n        M.pulseT = 0;\n        M.pulseLastX = lay.width / 2 - lay.textWidth * 0.62;\n        wakeRef.current();\n      }\n      draw();\n    };\n    document.fonts.load(`${fontWeight} 100px ${family}`, text).then(run, run);\n    return () => {\n      cancelled = true;\n    };\n  }, [width, text, fontWeight, fontFamily, fontSize, fontsReady, frozen, still, pulseOnMount, epoch, draw, advance, clearFields]);\n\n  // Pointer input.\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    if (!wrap || still || frozen || !supported) return;\n    const toLocal = (e: PointerEvent): [number, number] | null => {\n      const lay = layoutRef.current;\n      if (!lay) return null;\n      const rect = wrap.getBoundingClientRect();\n      const sx = rect.width > 0 ? lay.width / rect.width : 1;\n      const sy = rect.height > 0 ? lay.height / rect.height : 1;\n      return [(e.clientX - rect.left) * sx, lay.height - (e.clientY - rect.top) * sy];\n    };\n    const onMove = (e: PointerEvent) => {\n      const pt = toLocal(e);\n      if (!pt) return;\n      const M = motionRef.current;\n      if (!M.hasPointer) {\n        M.lastX = pt[0];\n        M.lastY = pt[1];\n      }\n      M.px = pt[0];\n      M.py = pt[1];\n      M.hasPointer = true;\n      M.hoverTarget = 1;\n      wakeRef.current();\n    };\n    const onLeave = () => {\n      const M = motionRef.current;\n      M.hasPointer = false;\n      M.hoverTarget = 0;\n      wakeRef.current();\n    };\n    const onUp = (e: PointerEvent) => {\n      if (e.pointerType !== \"mouse\") onLeave();\n    };\n    wrap.addEventListener(\"pointermove\", onMove);\n    wrap.addEventListener(\"pointerdown\", onMove);\n    wrap.addEventListener(\"pointerleave\", onLeave);\n    wrap.addEventListener(\"pointercancel\", onLeave);\n    wrap.addEventListener(\"pointerup\", onUp);\n    return () => {\n      wrap.removeEventListener(\"pointermove\", onMove);\n      wrap.removeEventListener(\"pointerdown\", onMove);\n      wrap.removeEventListener(\"pointerleave\", onLeave);\n      wrap.removeEventListener(\"pointercancel\", onLeave);\n      wrap.removeEventListener(\"pointerup\", onUp);\n    };\n  }, [still, frozen, supported]);\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      pulse: () => {\n        const lay = layoutRef.current;\n        if (!lay || still || frozen) return;\n        const M = motionRef.current;\n        M.pulseT = 0;\n        M.pulseLastX = lay.width / 2 - lay.textWidth * 0.62;\n        wakeRef.current();\n      },\n    }),\n    [still, frozen],\n  );\n\n  const label = ariaLabel ?? text;\n\n  return (\n    <div\n      ref={wrapRef}\n      role=\"img\"\n      aria-label={label}\n      data-tone={resolvedTone}\n      className={cn(\"relative w-full select-none\", className)}\n      style={{\n        height: layout ? layout.height : undefined,\n        minHeight: layout ? undefined : \"1.3em\",\n        touchAction: \"pan-y\",\n        fontFamily: fontFamily ?? \"var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif\",\n        fontWeight,\n      }}\n    >\n      {supported ? (\n        <canvas ref={canvasRef} aria-hidden=\"true\" className=\"absolute left-0 top-0 block\" />\n      ) : (\n        <span\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 flex items-center justify-center whitespace-nowrap leading-none\"\n          style={{\n            color: color ?? palette.text,\n            fontSize: layout ? layout.fontSize : \"clamp(40px, 12vw, 140px)\",\n            letterSpacing: \"-0.03em\",\n          }}\n        >\n          {text}\n        </span>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/text/chromatic-text.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    },
    {
      "path": "components/bjork-ui/_core/canvas.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState, type RefObject } from \"react\";\nimport { isCoarsePointer } from \"./loop\";\n\n// Caps DPR at 1.5 on coarse pointers and 2 otherwise.\nexport function defaultMaxDpr(): number {\n  return isCoarsePointer() ? 1.5 : 2;\n}\n\n// Sets the backing size for a CSS box and returns the DPR used.\nexport function sizeCanvas(\n  canvas: HTMLCanvasElement,\n  cssW: number,\n  cssH: number,\n  maxDpr: number = defaultMaxDpr(),\n): number {\n  const dpr = Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, maxDpr);\n  canvas.width = Math.max(1, Math.round(cssW * dpr));\n  canvas.height = Math.max(1, Math.round(cssH * dpr));\n  canvas.style.width = `${cssW}px`;\n  canvas.style.height = `${cssH}px`;\n  return dpr;\n}\n\n// Rounded size of an element, kept current with a ResizeObserver.\nexport function useElementSize(ref: RefObject<HTMLElement | null>): { width: number; height: number } {\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const box = entries[0]?.contentRect;\n      if (!box) return;\n      const width = Math.round(box.width);\n      const height = Math.round(box.height);\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return size;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/canvas.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    }
  ],
  "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": "Text",
    "registryUrl": "https://ui.isaiahbjork.com/chromatic-text.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/chromatic-text.json"
  },
  "type": "registry:component"
}