{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mosaic-settle",
  "title": "Mosaic settle",
  "description": "Mosaic settle is a production-ready galleries component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/galleries/mosaic-settle.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\nimport NextImage from \"next/image\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { useVisibleLoop } from \"@/components/bjork-ui/_core/loop\";\nimport { useElementSize } from \"@/components/bjork-ui/_core/canvas\";\nimport { hashString } from \"@/components/bjork-ui/_core/random\";\n\nexport type MosaicPalette = \"ember\" | \"ocean\" | \"mono\" | [string, string, string, string];\n\nexport interface MosaicSettleProps {\n  src: string;\n  alt: string;\n  caption?: ReactNode;\n  pending?: boolean;\n  aspectRatio?: number;\n  rounded?: number;\n  palette?: MosaicPalette;\n  mechanic?: \"organic\" | \"sweep\";\n  /** Sweep origin in frame space, 0 to 1. [0, 0] is the top-left corner. */\n  origin?: [number, number];\n  cellSize?: number;\n  duration?: number;\n  onSettled?: () => void;\n  onError?: () => void;\n  /** Images that `attract` cycles through. Falls back to `src`. */\n  srcList?: string[];\n  /** Posed frame: renders this settle progress (0 = churn at t 0, 1 = fully resolved) once, with no loop. */\n  progress?: number;\n  tone?: BjorkTone;\n  attract?: boolean;\n  className?: string;\n}\n\ntype Rgb = [number, number, number];\n\nconst PALETTES: Record<\"ember\" | \"ocean\" | \"monoDark\" | \"monoLight\", string[]> = {\n  ember: [\"#140904\", \"#5a1f08\", \"#ec5c13\", \"#ffb27a\"],\n  ocean: [\"#04101a\", \"#0d3b5c\", \"#1f7fb0\", \"#9fd6f0\"],\n  monoDark: [\"#0b0b0b\", \"#2a2a2a\", \"#6a6a6a\", \"#d8d8d8\"],\n  monoLight: [\"#f7f5ef\", \"#e1d7c8\", \"#a49b8e\", \"#3a3a3a\"],\n};\n\n// Attract timeline: churn for 1.8s, then release (settle, fade and hold) for 2.6s, then the next image.\nconst ATTRACT_CHURN_MS = 1800;\nconst ATTRACT_RELEASE_MS = 2600;\nconst ATTRACT_RESUME_MS = 4000;\nconst FADE_MS = 200;\nconst FALLBACK_FADE_MS = 400;\n// Plan: DPR capped at 1.5.\nconst MAX_DPR = 1.5;\n// Decoded plates kept as textures on the shared context, so Regenerate and attract never re-upload.\nconst TEXTURE_CACHE = 12;\n// Grace period before the shared context is released, so a StrictMode or keyed remount reuses it.\nconst DISPOSE_DELAY_MS = 1000;\n\nfunction hexToRgb(hex: string): Rgb {\n  let h = hex.trim().replace(\"#\", \"\");\n  if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n  const n = parseInt(h.slice(0, 6), 16);\n  if (Number.isNaN(n)) return [0, 0, 0];\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nfunction resolvePalette(palette: MosaicPalette, tone: BjorkTone): string[] {\n  if (Array.isArray(palette)) return palette;\n  if (palette === \"mono\") return tone === \"light\" ? PALETTES.monoLight : PALETTES.monoDark;\n  return PALETTES[palette];\n}\n\n// Module-level sources: compiled once, shared by every instance.\nconst VERT = /* glsl */ `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n  vUv = position * 0.5 + 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = /* glsl */ `\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uTex;\nuniform float uSettle;\nuniform float uTime;\nuniform vec2 uCells;\nuniform vec3 uPalette[4];\nuniform float uMechanic;\nuniform vec2 uOrigin;\nuniform float uImageAspect;\nuniform float uFrameAspect;\nuniform float uSeed;\n\nfloat hash(vec2 p) {\n  p = fract(p * vec2(443.897, 441.423));\n  p += dot(p, p.yx + 19.19);\n  return fract((p.x + p.y) * p.x);\n}\n\n// Value noise in 0..1.\nfloat vnoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nvec3 paletteAt(float i) {\n  if (i < 0.5) return uPalette[0];\n  if (i < 1.5) return uPalette[1];\n  if (i < 2.5) return uPalette[2];\n  return uPalette[3];\n}\n\n// Cover fit: maps frame uv to image uv, the same crop as object-fit: cover.\nvec2 coverUv(vec2 uv) {\n  float r = uFrameAspect / uImageAspect;\n  vec2 o = uv;\n  if (r > 1.0) o.y = (uv.y - 0.5) / r + 0.5;\n  else o.x = (uv.x - 0.5) * r + 0.5;\n  return o;\n}\n\nfloat originDist(vec2 p) {\n  return length((p - uOrigin) * vec2(uFrameAspect, 1.0));\n}\n\nfloat maxOriginDist() {\n  return max(max(originDist(vec2(0.0, 0.0)), originDist(vec2(1.0, 0.0))),\n             max(originDist(vec2(0.0, 1.0)), originDist(vec2(1.0, 1.0))));\n}\n\nvoid main() {\n  vec2 cell = floor(vUv * uCells);\n  vec2 cc = (cell + 0.5) / uCells;\n  float h = hash(cell + uSeed);\n\n  // Pending churn: quantised noise picks a palette index, plus a small brightness flicker.\n  float n = vnoise(cell * 0.35 + vec2(uTime * 0.6 + uSeed, uSeed * 0.37));\n  float idx = floor(clamp(n, 0.0, 0.999) * 4.0);\n  vec3 pend = paletteAt(idx) + (h - 0.5) * 0.08;\n\n  // Settle delay per cell.\n  float delay;\n  if (uMechanic < 0.5) {\n    delay = 0.6 * h + 0.4 * vnoise(cell * 0.12 + uSeed);\n  } else {\n    delay = 0.85 * originDist(cc) / maxOriginDist() + 0.15 * h;\n  }\n  float local = smoothstep(delay, delay + 0.15, uSettle * 1.15);\n\n  // Cells sample the image at their centre, then de-pixelate over the last 15 percent.\n  float depix = smoothstep(0.85, 1.0, uSettle);\n  vec2 suv = mix(cc, vUv, depix);\n  vec3 texel = texture2D(uTex, coverUv(suv)).rgb;\n\n  gl_FragColor = vec4(mix(pend, texel, local), 1.0);\n}\n`;\n\n// One WebGL context and one compiled program for every MosaicSettle on the page. Each frame renders into a\n// corner of the shared drawing buffer and copies it onto its own 2D canvas, so Replay and Regenerate never\n// create a context or compile a shader. Contexts are a scarce browser resource (Chrome keeps about 16).\ninterface SharedGL {\n  canvas: HTMLCanvasElement;\n  gl: WebGLRenderingContext;\n  program: WebGLProgram;\n  buffer: WebGLBuffer;\n  u: Record<\n    | \"uTex\"\n    | \"uSettle\"\n    | \"uTime\"\n    | \"uCells\"\n    | \"uPalette\"\n    | \"uMechanic\"\n    | \"uOrigin\"\n    | \"uImageAspect\"\n    | \"uFrameAspect\"\n    | \"uSeed\",\n    WebGLUniformLocation | null\n  >;\n  placeholder: WebGLTexture;\n  textures: Map<string, WebGLTexture>;\n  width: number;\n  height: number;\n  lost: boolean;\n}\n\nlet shared: SharedGL | null = null;\nlet sharedUnsupported = false;\nlet sharedUsers = 0;\nlet disposeTimer = 0;\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  return shader;\n}\n\nfunction createShared(): SharedGL | null {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = 1;\n  canvas.height = 1;\n  let gl: WebGLRenderingContext | null = null;\n  try {\n    gl = canvas.getContext(\"webgl\", {\n      alpha: false,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      premultipliedAlpha: false,\n      preserveDrawingBuffer: false,\n    });\n  } catch {\n    gl = null;\n  }\n  if (!gl) return null;\n\n  const vs = compile(gl, gl.VERTEX_SHADER, VERT);\n  const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);\n  const program = gl.createProgram();\n  const buffer = gl.createBuffer();\n  const placeholder = gl.createTexture();\n  if (!vs || !fs || !program || !buffer || !placeholder) return null;\n  gl.attachShader(program, vs);\n  gl.attachShader(program, fs);\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    return null;\n  }\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  gl.useProgram(program);\n\n  // One oversized triangle covers the viewport.\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"position\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n  // A 1x1 placeholder keeps the sampler complete until the real image is uploaded.\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, placeholder);\n  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));\n  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n\n  const loc = (name: string) => gl.getUniformLocation(program, name);\n  const state: SharedGL = {\n    canvas,\n    gl,\n    program,\n    buffer,\n    u: {\n      uTex: loc(\"uTex\"),\n      uSettle: loc(\"uSettle\"),\n      uTime: loc(\"uTime\"),\n      uCells: loc(\"uCells\"),\n      uPalette: loc(\"uPalette\"),\n      uMechanic: loc(\"uMechanic\"),\n      uOrigin: loc(\"uOrigin\"),\n      uImageAspect: loc(\"uImageAspect\"),\n      uFrameAspect: loc(\"uFrameAspect\"),\n      uSeed: loc(\"uSeed\"),\n    },\n    placeholder,\n    textures: new Map(),\n    width: 1,\n    height: 1,\n    lost: false,\n  };\n  gl.uniform1i(state.u.uTex, 0);\n  canvas.addEventListener(\"webglcontextlost\", () => {\n    // Recreated on the next draw. Nothing on the lost context is touched again.\n    state.lost = true;\n    if (shared === state) shared = null;\n  });\n  return state;\n}\n\n// The shared context, created on first use. Null when WebGL is unavailable (the CSS fallback takes over).\nfunction getShared(): SharedGL | null {\n  if (shared && !shared.lost) return shared;\n  if (sharedUnsupported) return null;\n  shared = createShared();\n  if (!shared) sharedUnsupported = true;\n  return shared;\n}\n\nfunction disposeShared() {\n  const s = shared;\n  shared = null;\n  if (!s || s.lost) return;\n  const { gl } = s;\n  s.textures.forEach((tex) => gl.deleteTexture(tex));\n  s.textures.clear();\n  gl.deleteTexture(s.placeholder);\n  gl.deleteBuffer(s.buffer);\n  gl.deleteProgram(s.program);\n  gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n}\n\nfunction retainShared() {\n  sharedUsers++;\n  window.clearTimeout(disposeTimer);\n}\n\nfunction releaseShared() {\n  sharedUsers = Math.max(0, sharedUsers - 1);\n  if (sharedUsers > 0) return;\n  window.clearTimeout(disposeTimer);\n  disposeTimer = window.setTimeout(() => {\n    if (sharedUsers === 0) disposeShared();\n  }, DISPOSE_DELAY_MS);\n}\n\n// Texture for a decoded image, uploaded once per source and kept in a small LRU shared by every frame.\nfunction textureFor(s: SharedGL, img: HTMLImageElement): WebGLTexture {\n  const { gl } = s;\n  const key = img.src;\n  const hit = s.textures.get(key);\n  if (hit) {\n    s.textures.delete(key);\n    s.textures.set(key, hit);\n    return hit;\n  }\n  const tex = gl.createTexture();\n  if (!tex) return s.placeholder;\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  // Throws a SecurityError for a cross-origin image without CORS; the caller falls back to the CSS surface.\n  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);\n  s.textures.set(key, tex);\n  if (s.textures.size > TEXTURE_CACHE) {\n    const [oldKey, oldTex] = s.textures.entries().next().value as [string, WebGLTexture];\n    s.textures.delete(oldKey);\n    gl.deleteTexture(oldTex);\n  }\n  return tex;\n}\n\ninterface LiveState {\n  pending: boolean;\n  reduce: boolean;\n  decoded: HTMLImageElement | null;\n  duration: number;\n  width: number;\n  height: number;\n  cols: number;\n  rows: number;\n  mechanic: \"organic\" | \"sweep\";\n  ox: number;\n  oy: number;\n  pal: Float32Array;\n  src: string;\n  seed: number;\n  progress: number | undefined;\n}\n\nconst INITIAL_LIVE: LiveState = {\n  pending: false,\n  reduce: false,\n  decoded: null,\n  duration: 900,\n  width: 0,\n  height: 0,\n  cols: 1,\n  rows: 1,\n  mechanic: \"organic\",\n  ox: 0,\n  oy: 0,\n  pal: new Float32Array(12),\n  src: \"\",\n  seed: 0,\n  progress: undefined,\n};\n\n// The visible surface of one frame: a 2D canvas the shared render is copied onto.\ninterface View {\n  canvas: HTMLCanvasElement;\n  ctx: CanvasRenderingContext2D;\n  dead: boolean;\n}\n\n// Renders one frame on the shared context and copies it to the view. Returns false when WebGL is gone.\nfunction draw(view: View, s: LiveState, settle: number, time: number): boolean {\n  const g = getShared();\n  if (!g) return false;\n  const { gl, u } = g;\n  const pw = view.canvas.width;\n  const ph = view.canvas.height;\n  if (pw < 1 || ph < 1) return true;\n  // The shared buffer only grows, so frames of different sizes never reallocate it per draw.\n  if (pw > g.width || ph > g.height) {\n    g.width = Math.max(g.width, pw);\n    g.height = Math.max(g.height, ph);\n    g.canvas.width = g.width;\n    g.canvas.height = g.height;\n  }\n  gl.viewport(0, 0, pw, ph);\n  gl.activeTexture(gl.TEXTURE0);\n  if (s.decoded) gl.bindTexture(gl.TEXTURE_2D, textureFor(g, s.decoded));\n  else gl.bindTexture(gl.TEXTURE_2D, g.placeholder);\n  gl.uniform1f(u.uTime, time);\n  gl.uniform1f(u.uSettle, settle);\n  gl.uniform2f(u.uCells, s.cols, s.rows);\n  gl.uniform1f(u.uFrameAspect, s.width / s.height);\n  gl.uniform1f(u.uImageAspect, s.decoded ? s.decoded.naturalWidth / s.decoded.naturalHeight : 1);\n  gl.uniform1f(u.uMechanic, s.mechanic === \"sweep\" ? 1 : 0);\n  gl.uniform2f(u.uOrigin, s.ox, s.oy);\n  gl.uniform3fv(u.uPalette, s.pal);\n  gl.uniform1f(u.uSeed, s.seed);\n  gl.drawArrays(gl.TRIANGLES, 0, 3);\n  // The viewport sits at the bottom-left of the buffer, which is the bottom rows of the canvas image.\n  view.ctx.drawImage(g.canvas, 0, g.height - ph, pw, ph, 0, 0, pw, ph);\n  return true;\n}\n\nfunction subscribeNoop() {\n  return () => {};\n}\nfunction getMounted() {\n  return true;\n}\nfunction getServerMounted() {\n  return false;\n}\n\nexport function MosaicSettle({\n  src,\n  alt,\n  caption,\n  pending = false,\n  aspectRatio = 4 / 3,\n  rounded = 16,\n  palette = \"ember\",\n  mechanic = \"organic\",\n  origin = [0, 0],\n  cellSize = 14,\n  duration = 900,\n  onSettled,\n  onError,\n  srcList,\n  progress,\n  tone: toneProp,\n  attract = false,\n  className,\n}: MosaicSettleProps) {\n  const tone = useBjorkTone(toneProp);\n  // Hydration-safe: the server and first client render assume no reduced motion.\n  const reducedRaw = useReducedMotion();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  const reduce = mounted && reducedRaw === true;\n\n  // Attract: cycle through srcList, churning, then releasing. Timers, not frames, so it stays cheap.\n  const list = srcList && srcList.length > 0 ? srcList : [src];\n  const attractOn = attract && !reduce;\n  const [attractStep, setAttractStep] = useState<0 | 1>(0);\n  const [attractIdx, setAttractIdx] = useState(0);\n  const [attractPaused, setAttractPaused] = useState(false);\n  const resumeTimer = useRef(0);\n\n  const effSrc = attractOn ? list[attractIdx % list.length] : src;\n  const effPending = pending || (attractOn && attractStep === 0);\n\n  const [decoded, setDecoded] = useState<{ src: string; img: HTMLImageElement } | null>(null);\n  const [failed, setFailed] = useState<string | null>(null);\n  const [doneFor, setDoneFor] = useState<string | null>(null);\n  const [fadeOn, setFadeOn] = useState(false);\n  const [glFailed, setGlFailed] = useState(false);\n  const [prevPending, setPrevPending] = useState(effPending);\n\n  // A rising edge of `pending` after the image has settled re-churns: clear the done latch.\n  if (prevPending !== effPending) {\n    setPrevPending(effPending);\n    if (effPending) {\n      setDoneFor(null);\n      setFadeOn(false);\n    }\n  }\n\n  const decodedImg = decoded && decoded.src === effSrc ? decoded.img : null;\n  const isFailed = failed === effSrc;\n  const settled = doneFor === effSrc;\n  const fallbackMode = glFailed && !isFailed;\n  const showCanvas = !glFailed && !isFailed && !settled;\n  const fallbackDone = fallbackMode && decodedImg !== null && !effPending;\n  const done = !isFailed && (fallbackMode ? fallbackDone : settled);\n  const busy = !isFailed && !done;\n  const imgShown = !isFailed && (fallbackMode ? fallbackDone : settled || fadeOn);\n\n  const figureRef = useRef<HTMLElement>(null);\n  const frameRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const viewRef = useRef<View | null>(null);\n  const phaseRef = useRef<\"churn\" | \"settle\">(\"churn\");\n  const phaseSrcRef = useRef(\"\");\n  const settleStartRef = useRef(0);\n  const live = useRef<LiveState>(INITIAL_LIVE);\n  const cbRef = useRef<{ onSettled?: () => void; onError?: () => void }>({});\n\n  const size = useElementSize(frameRef);\n  const colors = resolvePalette(palette, tone);\n  const tokens = BJORK_PALETTE[tone];\n  const cssVars = {\n    \"--bjork-border\": tokens.border,\n    \"--bjork-text-muted\": tokens.textMuted,\n    \"--bjork-text-soft\": tokens.textSoft,\n    \"--bjork-accent\": tokens.accent,\n  } as CSSProperties;\n  const gradient = `linear-gradient(135deg, ${colors[0]} 0%, ${colors[1]} 35%, ${colors[2]} 68%, ${colors[3]} 100%)`;\n\n  // Effects run in declaration order. Keep the live snapshot first so later effects see fresh values.\n  useEffect(() => {\n    cbRef.current = { onSettled, onError };\n    live.current = {\n      pending: effPending,\n      reduce,\n      decoded: decodedImg,\n      duration,\n      width: size.width,\n      height: size.height,\n      cols: Math.max(1, Math.round(size.width / cellSize)),\n      rows: Math.max(1, Math.round(size.height / cellSize)),\n      mechanic,\n      ox: origin[0],\n      oy: 1 - origin[1],\n      pal: new Float32Array(colors.flatMap(hexToRgb)),\n      src: effSrc,\n      // Per-image offset, so frames side by side never churn in lockstep.\n      seed: (hashString(effSrc) % 997) / 7,\n      progress,\n    };\n  });\n\n  // Decode once per source. The texture is uploaded from this decoded image.\n  useEffect(() => {\n    let alive = true;\n    const img = new Image();\n    img.src = effSrc;\n    img.decode().then(\n      () => {\n        if (alive) setDecoded({ src: effSrc, img });\n      },\n      () => {\n        if (alive) {\n          setFailed(effSrc);\n          cbRef.current.onError?.();\n        }\n      },\n    );\n    return () => {\n      alive = false;\n    };\n  }, [effSrc]);\n\n  // Holds the shared WebGL context for as long as this frame is mounted; the last unmount releases it.\n  useEffect(() => {\n    retainShared();\n    return releaseShared;\n  }, []);\n\n  // The 2D view canvas mounts while unsettled. The shared context is created (and the program compiled) on the\n  // first mount only; later churns reuse it.\n  useEffect(() => {\n    if (!showCanvas) return;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = getShared() ? canvas.getContext(\"2d\") : null;\n    if (!ctx) {\n      requestAnimationFrame(() => setGlFailed(true));\n      return;\n    }\n    const view: View = { canvas, ctx, dead: false };\n    viewRef.current = view;\n    phaseRef.current = \"churn\";\n    phaseSrcRef.current = \"\";\n    return () => {\n      // Mark dead first so a frame that is already queued never draws to an unmounted canvas.\n      view.dead = true;\n      if (viewRef.current === view) viewRef.current = null;\n    };\n  }, [showCanvas]);\n\n  // One frame function drives churn, settle and the static reduced-motion frame.\n  const frame = (_dt: number, t: number): boolean => {\n    const s = live.current;\n    const view = viewRef.current;\n    if (!view || view.dead) return false;\n    if (!s.width || !s.height) return true;\n\n    // Posed: one static frame at the given progress, then idle. Redrawn when the image decodes or progress changes.\n    if (s.progress !== undefined) {\n      try {\n        if (!draw(view, s, Math.min(1, Math.max(0, s.progress)), 0)) setGlFailed(true);\n      } catch {\n        setGlFailed(true);\n      }\n      return false;\n    }\n\n    if (phaseSrcRef.current !== s.src) {\n      phaseSrcRef.current = s.src;\n      phaseRef.current = \"churn\";\n    }\n    if (s.pending && phaseRef.current === \"settle\") phaseRef.current = \"churn\";\n    if (phaseRef.current === \"churn\" && s.decoded && !s.pending) {\n      phaseRef.current = \"settle\";\n      settleStartRef.current = t;\n    }\n\n    let settle = 0;\n    let finished = false;\n    if (phaseRef.current === \"settle\") {\n      if (s.reduce) {\n        settle = 1;\n        finished = true;\n      } else {\n        const p = Math.min(1, (t - settleStartRef.current) / (s.duration / 1000));\n        finished = p >= 1;\n        // easeOutQuad: the cell flip spreads over most of the duration (ease.out front-loads it into ~250ms).\n        settle = finished ? 1 : p * (2 - p);\n      }\n    }\n\n    try {\n      // Reduced motion draws one static frame: no churn time, no settle sweep.\n      if (!draw(view, s, settle, s.reduce ? 0 : t)) {\n        setGlFailed(true);\n        return false;\n      }\n    } catch {\n      // Texture upload can fail on cross-origin images without CORS. Fall back to the CSS surface.\n      setGlFailed(true);\n      return false;\n    }\n\n    if (finished) {\n      const srcAtFinish = s.src;\n      setFadeOn(true);\n      window.setTimeout(() => {\n        setFadeOn(false);\n        if (!live.current.pending) setDoneFor(srcAtFinish);\n      }, FADE_MS);\n      return false;\n    }\n    if (phaseRef.current === \"churn\" && s.reduce) return false;\n    return true;\n  };\n\n  // The loop is always subscribed. With no canvas the frame returns false at once, so data-loop reads idle.\n  const { wake } = useVisibleLoop(figureRef, frame);\n\n  // Sizes the view canvas (DPR capped at 1.5). Resizing clears it, so this also wakes a posed, idle frame.\n  useEffect(() => {\n    const view = viewRef.current;\n    if (!view || view.dead || !showCanvas || !size.width || !size.height) return;\n    const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n    const pw = Math.max(1, Math.round(size.width * dpr));\n    const ph = Math.max(1, Math.round(size.height * dpr));\n    if (view.canvas.width !== pw || view.canvas.height !== ph) {\n      view.canvas.width = pw;\n      view.canvas.height = ph;\n    }\n    wake();\n  }, [size.width, size.height, showCanvas, wake]);\n\n  useEffect(() => {\n    wake();\n  }, [wake, effPending, decodedImg, reduce, progress, showCanvas]);\n\n  // Fires once per completed settle (canvas or fallback), including re-settles after a re-churn.\n  useEffect(() => {\n    if (done) cbRef.current.onSettled?.();\n  }, [done]);\n\n  // Attract timeline: each step is a timer, so nothing accumulates over time.\n  useEffect(() => {\n    if (!attractOn || attractPaused) return;\n    const id = window.setTimeout(\n      () => {\n        if (attractStep === 0) {\n          setAttractStep(1);\n        } else {\n          setAttractStep(0);\n          setAttractIdx((i) => (i + 1) % list.length);\n        }\n      },\n      attractStep === 0 ? ATTRACT_CHURN_MS : ATTRACT_RELEASE_MS,\n    );\n    return () => window.clearTimeout(id);\n  }, [attractOn, attractPaused, attractStep, list.length]);\n\n  const pauseAttract = () => {\n    if (!attractOn) return;\n    setAttractPaused(true);\n    window.clearTimeout(resumeTimer.current);\n    resumeTimer.current = window.setTimeout(() => setAttractPaused(false), ATTRACT_RESUME_MS);\n  };\n\n  return (\n    <figure\n      ref={figureRef}\n      aria-busy={busy}\n      onPointerMove={pauseAttract}\n      onPointerDown={pauseAttract}\n      onKeyDown={pauseAttract}\n      className={cn(\"m-0 w-full min-w-0\", className)}\n      style={cssVars}\n    >\n      <div\n        ref={frameRef}\n        className=\"relative w-full overflow-hidden border border-[color:var(--bjork-border)]\"\n        style={{ aspectRatio, borderRadius: rounded, background: colors[0] }}\n      >\n        {fallbackMode || isFailed ? (\n          <div aria-hidden=\"true\" className=\"absolute inset-0\" style={{ background: gradient }} />\n        ) : null}\n        <NextImage\n          src={effSrc}\n          alt={alt}\n          fill\n          unoptimized\n          loading=\"eager\"\n          sizes=\"(max-width: 768px) 100vw, 560px\"\n          draggable={false}\n          className=\"object-cover\"\n          style={{\n            opacity: imgShown ? 1 : 0,\n            filter: fallbackMode && !imgShown && !reduce ? \"blur(12px)\" : \"none\",\n            transition: fallbackMode\n              ? `opacity ${reduce ? 200 : FALLBACK_FADE_MS}ms cubic-bezier(0.23,1,0.32,1), filter ${FALLBACK_FADE_MS}ms cubic-bezier(0.23,1,0.32,1)`\n              : \"none\", // the canvas fades out over an opaque img, so the swap never dips\n          }}\n        />\n        {showCanvas ? (\n          <canvas\n            ref={canvasRef}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 size-full\"\n            style={{ opacity: fadeOn ? 0 : 1, transition: `opacity ${FADE_MS}ms ease-out` }}\n          />\n        ) : null}\n        {isFailed ? (\n          <div className=\"absolute inset-0 flex items-center justify-center p-4\">\n            <span className=\"font-bjork-alpha text-[12px] text-[color:var(--bjork-text-soft)]\">\n              Image unavailable\n            </span>\n          </div>\n        ) : null}\n      </div>\n      {caption ? (\n        <figcaption className=\"mt-2 font-bjork-alpha text-[12px] text-[color:var(--bjork-text-muted)]\">\n          {caption}\n        </figcaption>\n      ) : null}\n    </figure>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/galleries/mosaic-settle.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/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/_core/canvas.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState, type RefObject } from \"react\";\nimport { isCoarsePointer } from \"./loop\";\n\n// Caps DPR at 1.5 on coarse pointers and 2 otherwise.\nexport function defaultMaxDpr(): number {\n  return isCoarsePointer() ? 1.5 : 2;\n}\n\n// Sets the backing size for a CSS box and returns the DPR used.\nexport function sizeCanvas(\n  canvas: HTMLCanvasElement,\n  cssW: number,\n  cssH: number,\n  maxDpr: number = defaultMaxDpr(),\n): number {\n  const dpr = Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, maxDpr);\n  canvas.width = Math.max(1, Math.round(cssW * dpr));\n  canvas.height = Math.max(1, Math.round(cssH * dpr));\n  canvas.style.width = `${cssW}px`;\n  canvas.style.height = `${cssH}px`;\n  return dpr;\n}\n\n// Rounded size of an element, kept current with a ResizeObserver.\nexport function useElementSize(ref: RefObject<HTMLElement | null>): { width: number; height: number } {\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const box = entries[0]?.contentRect;\n      if (!box) return;\n      const width = Math.round(box.width);\n      const height = Math.round(box.height);\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return size;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/canvas.ts"
    },
    {
      "path": "components/bjork-ui/_core/random.ts",
      "content": "// Seeded randomness and noise. Deterministic for a given seed, so previews and tests repeat.\n\n// Small fast PRNG. Returns floats in [0, 1).\nexport function mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n// FNV-1a 32-bit.\nexport function hashString(s: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < s.length; i++) {\n    h ^= s.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  return h >>> 0;\n}\n\n// Integer lattice hash to [-1, 1].\nfunction latticeHash(seed: number, x: number, y = 0): number {\n  let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(seed | 0, 2246822519);\n  h = Math.imul(h ^ (h >>> 13), 1274126177);\n  h ^= h >>> 16;\n  return ((h >>> 0) / 4294967295) * 2 - 1;\n}\n\nfunction smooth(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\nexport function valueNoise1D(seed: number): (x: number) => number {\n  return (x: number) => {\n    const i = Math.floor(x);\n    const f = x - i;\n    const a = latticeHash(seed, i);\n    const b = latticeHash(seed, i + 1);\n    return a + (b - a) * smooth(f);\n  };\n}\n\nexport function fbm1D(seed: number, octaves = 3): (x: number) => number {\n  const layers = Array.from({ length: octaves }, (_, o) => valueNoise1D(seed + o * 101));\n  let norm = 0;\n  for (let o = 0; o < octaves; o++) norm += 0.5 ** o;\n  return (x: number) => {\n    let sum = 0;\n    let amp = 1;\n    let freq = 1;\n    for (let o = 0; o < octaves; o++) {\n      sum += amp * layers[o](x * freq);\n      amp *= 0.5;\n      freq *= 2;\n    }\n    return sum / norm;\n  };\n}\n\nexport function valueNoise2D(seed: number): (x: number, y: number) => number {\n  return (x: number, y: number) => {\n    const xi = Math.floor(x);\n    const yi = Math.floor(y);\n    const fx = smooth(x - xi);\n    const fy = smooth(y - yi);\n    const a = latticeHash(seed, xi, yi);\n    const b = latticeHash(seed, xi + 1, yi);\n    const c = latticeHash(seed, xi, yi + 1);\n    const d = latticeHash(seed, xi + 1, yi + 1);\n    const top = a + (b - a) * fx;\n    const bottom = c + (d - c) * fx;\n    return top + (bottom - top) * fy;\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/random.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": "Galleries",
    "registryUrl": "https://ui.isaiahbjork.com/mosaic-settle.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/mosaic-settle.json"
  },
  "type": "registry:component"
}