{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-scene",
  "title": "Glass Scene",
  "description": "Draws the glass inside it as real bevelled glass that bends its backdrop and melts together.",
  "dependencies": [
    "clsx"
  ],
  "registryDependencies": [
    "@glassyui/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui/glass-scene.tsx",
      "content": "'use client';\n\nimport './styles/liquid.css';\n\nimport clsx from 'clsx';\nimport { forwardRef, useEffect, useMemo, useRef, useState } from 'react';\nimport type { CSSProperties, HTMLAttributes } from 'react';\nimport { mergeRefs } from './lib/merge-refs';\nimport {\n  GlassSceneContext,\n  createGlassSceneRegistry,\n  useGlassScenePane,\n} from './lib/scene-context';\nimport { createSceneRenderer } from './lib/scene-renderer';\nimport { useOptionalPreferences } from './glass-provider';\nimport type { SceneOptics, SceneRenderer, SceneSettings } from './lib/scene-renderer';\n\nexport type GlassSceneOptics = SceneOptics & {\n  /** Distance in CSS pixels within which neighbouring glass melts together. */\n  merge: number;\n};\n\nexport type GlassSceneProps = HTMLAttributes<HTMLDivElement> & {\n  /**\n   * The image the glass refracts, painted as the scene's background: a URL,\n   * or a CSS url() or var() value. Serve it from this origin or with CORS.\n   */\n  backdrop?: string;\n  /** Lets glass that sets --lg-scene-flow ripple like liquid. Reduced motion holds it still. */\n  flow?: boolean;\n  optics?: Partial<GlassSceneOptics>;\n};\n\nconst DEFAULT_OPTICS: GlassSceneOptics = {\n  ior: 1.5,\n  thickness: 40,\n  bevel: 28,\n  dispersion: 0.15,\n  frost: 1,\n  merge: 12,\n  specular: 1,\n};\n\nconst toCssUrl = (value: string) =>\n  /^\\s*(url|var)\\(/i.test(value)\n    ? value\n    : `url(\"${value.replace(/[\"\\\\\\n]/g, (character) => (character === '\\n' ? '\\\\a ' : `\\\\${character}`))}\")`;\n\n/**\n * Draws its glass children as one body of thick glass over a backdrop image:\n * rims bend the image and split its colours, nearby pieces melt together and\n * a fixed light catches the rims. Needs WebGL2; without it, and under reduced\n * transparency or increased contrast, every child keeps its CSS glass.\n */\nexport const GlassScene = forwardRef<HTMLDivElement, GlassSceneProps>(\n  ({ backdrop, flow = false, optics, className, style, children, ...rest }, forwardedRef) => {\n    const [registry] = useState(createGlassSceneRegistry);\n    const rootRef = useRef<HTMLDivElement | null>(null);\n    const canvasRef = useRef<HTMLCanvasElement | null>(null);\n    const backdropRef = useRef<HTMLSpanElement | null>(null);\n    const rendererRef = useRef<SceneRenderer | null>(null);\n    const ref = useMemo(() => mergeRefs(rootRef, forwardedRef), [forwardedRef]);\n    const { preferences } = useOptionalPreferences();\n    const scale = preferences.thickness;\n\n    // The site's thickness preference scales the default slab. Optics a scene sets itself stay as set.\n    const { ior, thickness, bevel, dispersion, frost, specular, merge } = {\n      ...DEFAULT_OPTICS,\n      thickness: DEFAULT_OPTICS.thickness * scale,\n      bevel: DEFAULT_OPTICS.bevel * (0.6 + 0.4 * scale),\n      dispersion: DEFAULT_OPTICS.dispersion * scale,\n      ...optics,\n    };\n\n    const settings = useMemo<SceneSettings>(\n      () => ({ flow, optics: { ior, thickness, bevel, dispersion, frost, specular } }),\n      [flow, ior, thickness, bevel, dispersion, frost, specular],\n    );\n\n    const settingsRef = useRef(settings);\n\n    useEffect(() => {\n      settingsRef.current = settings;\n      rendererRef.current?.update(settings);\n    }, [settings]);\n\n    useEffect(() => {\n      const root = rootRef.current;\n      const canvas = canvasRef.current;\n      const layer = backdropRef.current;\n\n      if (!root || !canvas || !layer) return;\n\n      const renderer = createSceneRenderer({ root, canvas, backdrop: layer, registry, settings: settingsRef.current });\n      rendererRef.current = renderer;\n\n      return () => {\n        rendererRef.current = null;\n        renderer.destroy();\n      };\n    }, [registry]);\n\n    return (\n      <GlassSceneContext.Provider value={registry}>\n        <div\n          ref={ref}\n          className={clsx('lg-scene', className)}\n          data-lg-scene-renderer=\"css\"\n          style={\n            // SAFETY: CSSProperties has no custom-property index; these add string-valued properties.\n            {\n              ...(backdrop ? { '--lg-scene-image': toCssUrl(backdrop) } : null),\n              '--lg-scene-merge': `${merge}px`,\n              ...style,\n            } as CSSProperties\n          }\n          {...rest}\n        >\n          <span ref={backdropRef} className=\"lg-scene-backdrop\" aria-hidden=\"true\" />\n          <canvas ref={canvasRef} className=\"lg-scene-canvas\" aria-hidden=\"true\" />\n          {children}\n        </div>\n      </GlassSceneContext.Provider>\n    );\n  },\n);\n\nGlassScene.displayName = 'GlassScene';\n\nexport type GlassDropProps = HTMLAttributes<HTMLDivElement> & {\n  /** Merge distance for this drop in CSS pixels. Defaults to the scene's. */\n  merge?: number;\n};\n\n/**\n * A drop of liquid glass inside a GlassScene. Size, round and place it with\n * CSS; drops closer than their merge distance melt into one.\n */\nexport const GlassDrop = forwardRef<HTMLDivElement, GlassDropProps>(\n  ({ merge, className, style, ...rest }, forwardedRef) => {\n    const dropRef = useRef<HTMLDivElement | null>(null);\n    const ref = useMemo(() => mergeRefs(dropRef, forwardedRef), [forwardedRef]);\n    // SAFETY: CSSProperties has no custom-property index; this adds one string-valued property.\n    const mergeStyle = { '--lg-scene-merge': `${merge}px`, ...style } as CSSProperties;\n\n    useGlassScenePane(dropRef);\n\n    return (\n      <div\n        ref={ref}\n        className={clsx('lg-scene-drop', className)}\n        style={merge === undefined ? style : mergeStyle}\n        {...rest}\n      />\n    );\n  },\n);\n\nGlassDrop.displayName = 'GlassDrop';\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-scene.tsx"
    },
    {
      "path": "registry/glassyui/lib/merge-refs.ts",
      "content": "import type { Ref } from 'react';\n\nexport function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T) => {\n    for (const ref of refs) {\n      if (!ref) continue;\n\n      if ('current' in ref) {\n        try {\n          ref.current = node;\n        } catch {\n          // Read-only refs cannot be assigned.\n        }\n      } else {\n        ref(node);\n      }\n    }\n  };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/merge-refs.ts"
    },
    {
      "path": "registry/glassyui/lib/scene-context.ts",
      "content": "'use client';\n\nimport { createContext, useContext, useLayoutEffect } from 'react';\nimport type { RefObject } from 'react';\n\nexport type GlassSceneRegistry = {\n  elements: ReadonlySet<HTMLElement>;\n  register: (element: HTMLElement) => () => void;\n  subscribe: (listener: () => void) => () => void;\n};\n\n// The registry exists before any effect runs, because children register in\n// their effects and those run before the scene's own effect.\nexport function createGlassSceneRegistry(): GlassSceneRegistry {\n  const elements = new Set<HTMLElement>();\n  const listeners = new Set<() => void>();\n  const notify = () => listeners.forEach((listener) => listener());\n\n  return {\n    elements,\n    register(element) {\n      elements.add(element);\n      notify();\n\n      return () => {\n        elements.delete(element);\n        notify();\n      };\n    },\n    subscribe(listener) {\n      listeners.add(listener);\n\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n  };\n}\n\nexport const GlassSceneContext = createContext<GlassSceneRegistry | null>(null);\n\n/**\n * Lets a glass element hand its material to the nearest GlassScene. Outside a\n * scene it does nothing. It registers before paint, so glass that mounts into\n * a live scene never shows its CSS material first.\n */\nexport function useGlassScenePane(ref: RefObject<HTMLElement | null>, enabled = true) {\n  const registry = useContext(GlassSceneContext);\n\n  useLayoutEffect(() => {\n    const element = ref.current;\n\n    if (!registry || !enabled || !element) return;\n\n    return registry.register(element);\n  }, [enabled, ref, registry]);\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/scene-context.ts"
    },
    {
      "path": "registry/glassyui/lib/scene-renderer.ts",
      "content": "import { createSpring, stepSpring } from './liquid-motion';\nimport type { Spring } from './liquid-motion';\nimport type { GlassSceneRegistry } from './scene-context';\n\n/*\n * The WebGL2 renderer behind GlassScene.\n *\n * Every registered glass element becomes a rounded box in one signed distance\n * field. Boxes fold together with a smooth minimum, so neighbours melt into a\n * single body when they come close. Each pixel inside the field is treated as\n * a thick slab of glass whose top curves down into a rounded bevel at the edge\n * and that floats a little above the scene's backdrop image. Light follows\n * Snell's law through the curved top, the slab, the flat underside and the air\n * gap, split into a spectrum of wavelengths, so the rim bends and fringes the\n * backdrop with colour where real glass would, and reflects all of it back\n * past the critical angle. The top reflects its surroundings with the exact\n * Fresnel term, tinted glass darkens and colours light with the length of its\n * path, and the shadow under tinted glass takes on its colour. Highlights come\n * from a fixed light above the top left, so the glass answers to what lies\n * behind it and never to where the pointer is. Glass that carries text keeps\n * it legible: where the backdrop would bring the face too close to the text\n * colour, the face dims or lifts just enough.\n *\n * Only the cells of the canvas that some glass can reach are drawn, and each\n * cell folds only the panes that reach it, so large scenes stay cheap.\n *\n * The canvas replaces the CSS material only while it really draws. Before the\n * first frame, and whenever WebGL2 is missing, lost or turned off by an\n * accessibility setting, elements keep their CSS glass. The root always says\n * which one shows in data-lg-scene-renderer: css, webgl or off.\n */\n\nexport type SceneOptics = {\n  /** Refractive index. Window glass is about 1.5. */\n  ior: number;\n  /** Height of the glass slab in CSS pixels. Thicker glass bends further. */\n  thickness: number;\n  /** Width of the curved rim in CSS pixels. */\n  bevel: number;\n  /**\n   * Spreads the refractive index by 0.4 times this value from red to violet\n   * light. Flint glass spreads it by about 0.05. 0 turns it off.\n   */\n  dispersion: number;\n  /** Multiplier on every element's frost radius. */\n  frost: number;\n  /** Strength of reflections and highlights. */\n  specular: number;\n};\n\nexport type SceneSettings = {\n  /** Lets elements that set --lg-scene-flow ripple and wobble like liquid. */\n  flow: boolean;\n  optics: SceneOptics;\n};\n\nexport type SceneRenderer = {\n  update: (settings: SceneSettings) => void;\n  destroy: () => void;\n};\n\ntype SceneRendererOptions = {\n  root: HTMLElement;\n  canvas: HTMLCanvasElement;\n  backdrop: HTMLElement;\n  registry: GlassSceneRegistry;\n  settings: SceneSettings;\n};\n\nconst MAX_PANES = 24;\n\n// The loop keeps drawing this long after the last sign of change, so a\n// transition that starts a frame late still gets every frame.\nconst TAIL_MS = 400;\n\n// Canvas and texture sizes stay bounded so a huge scene cannot exhaust a\n// phone's GPU memory.\nconst MAX_PIXELS = 4_200_000;\n\nconst MAX_TEXTURE = 4096;\n\n// The light sits above the top left, where interfaces conventionally light their surfaces.\nconst LIGHT = (() => {\n  const [x, y, z] = [-0.5, -0.7, 0.5];\n  const length = Math.hypot(x, y, z);\n\n  return [x / length, y / length, z / length];\n})();\n\n// Shadows fade out within this distance, which bounds the drawn area.\nconst SHADOW_REACH = 70;\n\n// The glass floats this far above the backdrop, so light that leaves its\n// underside at an angle travels on before it lands.\nconst AIR_GAP = 8;\n\n// The share of a tinted pane's shadow made of light that crossed the glass\n// and took on its colour.\nconst SHADOW_TINT = 0.3;\n\n// A flowing edge sways by up to this much per unit of flow.\nconst WOBBLE = 8;\n\n// A leaving pane below this presence is invisible and is dropped.\nconst CULL = 0.004;\n\n// Text on glass keeps this contrast with the face behind it. The margin over\n// WCAG's 4.5:1 absorbs rounding to 8-bit colour.\nconst INK_CONTRAST = 4.6;\n\nconst RASTER_THROTTLE_MS = 150;\n\n// Resolution steps the loop falls back to while frames run slow. An idle\n// scene always finishes with a full-resolution frame.\nconst SCALES = [1, 0.75, 0.5];\n\nconst WARNING = 'Glassy UI GlassScene: ';\n\nconst RENDERER = 'data-lg-scene-renderer';\n\nconst STATE = 'data-lg-scene-state';\n\nconst BACKDROP = 'data-lg-scene-backdrop';\n\nconst SCALE = 'data-lg-scene-scale';\n\nconst ACTIVE = 'data-lg-scene';\n\nconst warned = new Set<string>();\n\nconst warnOnce = (key: string, message: string) => {\n  if (warned.has(key)) return;\n\n  warned.add(key);\n  console.warn(WARNING + message);\n};\n\n// A golden-angle spiral spreads the frost taps evenly over a unit disk.\nconst DISK = Array.from({ length: 9 }, (_, index) => {\n  const radius = Math.sqrt((index + 0.5) / 9);\n  const angle = index * 2.39996;\n\n  return `vec2(${(radius * Math.cos(angle)).toFixed(5)}, ${(radius * Math.sin(angle)).toFixed(5)})`;\n}).join(', ');\n\n// Cauchy's law makes the index linear in 1 / λ². Measured from the index at\n// the helium d line, 587.6 nm, where glass indices are quoted, violet light\n// (400 nm) and red light (700 nm) sit at these shares of the full spread.\nconst VIOLET = (1 / 400 ** 2 - 1 / 587.6 ** 2) / (1 / 400 ** 2 - 1 / 700 ** 2);\n\nconst RED = VIOLET - 1;\n\n// Seven bands of the visible spectrum, each at its place from violet (0) to\n// red (1) in 1 / λ², with what it adds to red, green and blue: the CIE colour\n// matching functions taken to linear sRGB, negative lobes dropped. Every\n// channel's weights sum to 1, so light that does not split stays white.\nconst BANDS = [\n  [0.1472, 0.0346, 0, 0.3599],\n  [0.3827, 0, 0, 0.5828],\n  [0.5611, 0, 0.3283, 0.0573],\n  [0.6995, 0, 0.5228, 0],\n  [0.8089, 0.5476, 0.1489, 0],\n  [0.897, 0.3774, 0, 0],\n  [0.9689, 0.0405, 0, 0],\n];\n\n// Where each channel's light lands on average along the same line.\nconst CENTROID = [1, 2, 3].map(\n  (channel) => BANDS.reduce((sum, band) => sum + band[0] * band[channel], 0) / BANDS.reduce((sum, band) => sum + band[channel], 0),\n);\n\n// Each vertex carries a cell corner and the mask of panes that can reach the\n// cell. The mask is an integer, so it goes to the fragment shader flat.\nconst VERTEX_SHADER = `#version 300 es\nlayout(location = 0) in vec2 a_corner;\nlayout(location = 1) in uint a_mask;\nflat out uint v_mask;\nvoid main() {\n  v_mask = a_mask;\n  gl_Position = vec4(a_corner, 0.0, 1.0);\n}`;\n\n/*\n * Glass model, per pixel p in CSS pixels from the scene's top left:\n *\n * - Distance: each pane is a rounded box. The fold is Inigo Quilez's\n *   quadratic smooth minimum, h = max(k - |a - b|, 0) / k and\n *   d = min(a, b) - h² k / 4, which bridges panes closer than k. Material\n *   (tint, frost, bevel, alpha) blends with the same weight, so a bridge\n *   takes on the look of both sides. A pane can change the fold only within\n *   about k of the others, so a cell folds just the panes whose shadow and\n *   bridges can reach it.\n * - Height: with s the depth inside the edge and u = s / W across a bevel of\n *   width W, the surface rises as f(u) = (1 - (1 - u)^4)^(1/4): vertical at\n *   the edge, flat from u = 1. The slab is z = T (0.3 + 0.7 f) thick and its\n *   slope is 0.7 T / W · f'(u).\n * - Refraction: the view ray (0, 0, -1) bends through the surface normal into\n *   the glass (GLSL refract, eta = 1 / n), crosses the slab, bends again at\n *   the flat underside, where n sin θ = sin θ', and crosses an air gap to the\n *   backdrop. The rim bends rays toward the middle, so it magnifies the\n *   backdrop beneath it; past the steepest point the shift turns back, which\n *   gives the mirrored strip real glass shows at its edge. The flat face\n *   shows the backdrop in place.\n * - Total internal reflection: the underside's exact Fresnel term rises to 1\n *   at the critical angle, sin θ = 1 / n, and the backdrop fades into the\n *   surroundings the glass reflects back up.\n * - Dispersion: violet and red light each take their own path, with indices\n *   from Cauchy's law. Seven bands of the spectrum between the two add up to\n *   red, green and blue, so white light fans out into a rainbow where the\n *   glass tilts and stays white where it does not. Narrow fans use the three\n *   channel centroids, wide ones a blur that matches the band spacing.\n * - Frost: nine disk taps plus a matching mip level blur the refracted image,\n *   in proportion to the slab's thickness.\n * - Absorption: tinted glass passes (1 - a) of the light across its full\n *   thickness and, by Beer-Lambert, an exponential share of that along the\n *   path the ray really takes.\n * - Reflection: the top's exact Fresnel term, less its value head on so the\n *   face stays clear, mirrors the surroundings: the backdrop beside the glass\n *   where the mirrored ray points down, a bright blurred sky where it points\n *   up. The edge catches a fixed light above the top left in a hairline.\n *   Nothing brighter runs parallel to it inside the glass: a band there reads\n *   as a cushion, and what shows at the rim should be the bending.\n * - Shadow: a soft drop shadow. Part of it is light that crossed the glass, so\n *   tinted glass casts a shadow of its colour whose brightest channel darkens\n *   as much as under clear glass.\n * - Legibility: a pane with ink passes the luminance range its face may take\n *   while that ink keeps its contrast. In linear light the face scales down\n *   to the cap or mixes toward white up to the floor, leaving the outermost\n *   pixel of the rim at full light.\n */\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\nprecision highp int;\n\n#define MAX_PANES ${MAX_PANES}\n\nconst float SHADOW_REACH = ${SHADOW_REACH.toFixed(1)};\nconst float AIR_GAP = ${AIR_GAP.toFixed(1)};\nconst float SHADOW_TINT = ${SHADOW_TINT.toFixed(2)};\nconst float VIOLET = ${VIOLET.toFixed(5)};\nconst float RED = ${RED.toFixed(5)};\nconst vec3 LIGHT = vec3(${LIGHT.map((value) => value.toFixed(5)).join(', ')});\nconst vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\nconst vec2 DISK[9] = vec2[9](${DISK});\nconst float BAND_AT[7] = float[7](${BANDS.map(([at]) => at.toFixed(4)).join(', ')});\nconst vec3 BAND_RGB[7] = vec3[7](${BANDS.map(([, red, green, blue]) => `vec3(${red.toFixed(4)}, ${green.toFixed(4)}, ${blue.toFixed(4)})`).join(', ')});\nconst vec3 CENTROID = vec3(${CENTROID.map((value) => value.toFixed(4)).join(', ')});\n\nuniform sampler2D u_backdrop;\nuniform vec2 u_size;\nuniform vec2 u_backing;\nuniform vec4 u_texMap;\nuniform float u_baseLod;\nuniform float u_density;\nuniform vec4 u_rect[MAX_PANES];  // centre x, centre y, half width, half height\nuniform vec4 u_radii[MAX_PANES]; // corner radius, merge radius, frost radius, seed\nuniform vec4 u_tint[MAX_PANES];  // premultiplied\nuniform vec4 u_extra[MAX_PANES]; // flow, press, bevel width, alpha\nuniform vec2 u_face[MAX_PANES];  // luminance cap and floor for legible ink\nuniform vec4 u_optics; // ior, thickness, dispersion, specular\nuniform vec4 u_look;   // shadow, highlight, flow amplitude, pixel ratio\nuniform float u_time;\n\nflat in highp uint v_mask;\n\nout vec4 fragColor;\n\nvec3 sampleAt(vec2 q, float lod) {\n  return textureLod(u_backdrop, q * u_texMap.xy + u_texMap.zw, lod).rgb;\n}\n\n// Signed distance to a rounded box and its outward gradient.\nvec3 roundedBox(vec2 p, vec2 b, float r) {\n  vec2 s = vec2(p.x < 0.0 ? -1.0 : 1.0, p.y < 0.0 ? -1.0 : 1.0);\n  vec2 q = abs(p) - b + r;\n\n  if (q.x > 0.0 && q.y > 0.0) {\n    float l = length(q);\n    return vec3(l - r, s * q / l);\n  }\n\n  if (q.x > q.y) return vec3(q.x - r, s.x, 0.0);\n\n  return vec3(q.y - r, 0.0, s.y);\n}\n\nvec3 toLinear(vec3 c) {\n  return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\n\nvec3 toSrgb(vec3 c) {\n  return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n// Dims a face brighter than cap and lifts one darker than lift, so the text\n// on it keeps its contrast whatever lies behind.\nvec3 legible(vec3 c, float cap, float lift) {\n  vec3 l = toLinear(c);\n  float y = dot(l, LUMA);\n\n  if (y > cap) l *= cap / y;\n  else if (y < lift) l = mix(l, vec3(1.0), (lift - y) / max(1.0 - y, 1e-4));\n\n  return toSrgb(l);\n}\n\n// A logistic step, 1 / (1 + e^(1.7 x / sigma)), in base 2.\nfloat softStep(float x, float sigma) {\n  return 1.0 / (1.0 + exp2(clamp(2.4526 * x / sigma, -40.0, 40.0)));\n}\n\n// Slow travelling waves bend a flowing pane's outline, by up to 7.95 pixels\n// along each axis per unit of flow.\nvec2 warp(vec2 p, float t, float seed) {\n  return vec2(\n    sin(p.y / 90.0 + t * 0.9 + seed) + 0.5 * sin(p.x / 47.0 - t * 1.3 + seed * 1.7),\n    sin(p.x / 90.0 - t * 0.8 + seed * 2.3) + 0.5 * sin(p.y / 47.0 + t * 1.1 + seed * 0.6)\n  ) * 5.3;\n}\n\n// Surface ripples tilt a flowing pane's normals.\nvec2 ripple(vec2 p, float t) {\n  vec2 a = p / 38.0;\n  float c = cos(a.x * 0.7 + a.y * 0.9 - t * 1.1) * 0.4;\n\n  return vec2(cos(a.x + t * 1.7) * 0.6 + c * 0.7, cos(a.y - t * 1.3) * 0.6 + c * 0.9);\n}\n\n// Unpolarised Fresnel reflectance where light passes from index a into index\n// b, arriving at cos i and leaving at cos t. Past the critical angle cos t is\n// 0 and it reflects everything.\nfloat fresnel(float a, float b, float cosI, float cosT) {\n  float s = (a * cosI - b * cosT) / (a * cosI + b * cosT);\n  float q = (a * cosT - b * cosI) / (a * cosT + b * cosI);\n\n  return 0.5 * (s * s + q * q);\n}\n\n// Follows the view ray down through glass of index n that is z thick here.\n// Returns where it lands on the backdrop, as a shift from where it entered;\n// 1 / cos of its angle inside, which stretches the path through the glass;\n// and the share of light the underside lets out, relative to head on.\nvec4 through(vec3 N, float n, float z) {\n  vec3 inside = refract(vec3(0.0, 0.0, -1.0), N, 1.0 / n);\n  float cosB = max(-inside.z, 0.05);\n  float sinOut = n * length(inside.xy);\n  float cosOut = sqrt(max(1.0 - sinOut * sinOut, 0.0));\n  // Past the critical angle the ray lands as if it had left at grazing\n  // angle, so the backdrop stays continuous as it fades out.\n  vec2 leaving = n * inside.xy / max(sinOut, 1.0);\n  vec2 shift = inside.xy * (z / cosB) + leaving * (AIR_GAP / max(cosOut, 0.1));\n  float head = (n - 1.0) / (n + 1.0);\n\n  return vec4(shift, 1.0 / cosB, clamp((1.0 - fresnel(n, 1.0, cosB, cosOut)) / (1.0 - head * head), 0.0, 1.0));\n}\n\n// The shadow under glass of this tint, premultiplied.\nvec4 castShadow(vec2 p, float shadow, vec4 tint) {\n  vec3 filtered = 1.0 - tint.a + tint.rgb;\n  filtered /= max(max(filtered.r, filtered.g), max(filtered.b, 1e-4));\n\n  if (shadow < 0.002 || min(min(filtered.r, filtered.g), filtered.b) > 0.996) return vec4(0.0, 0.0, 0.0, shadow);\n\n  float strength = min(shadow / (1.0 - SHADOW_TINT), 1.0);\n\n  return vec4(sampleAt(p, u_baseLod) * filtered * (strength * SHADOW_TINT), strength);\n}\n\nvoid main() {\n  vec2 p = vec2(gl_FragCoord.x, u_backing.y - gl_FragCoord.y) * (u_size / u_backing);\n  float t = u_time;\n  float amp = u_look.z;\n\n  float d = 1e5;\n  vec2 g = vec2(0.0, -1.0);\n  vec4 tint = vec4(0.0);\n  float frost = 0.0;\n  float bevel = 1.0;\n  float flow = 0.0;\n  float press = 0.0;\n  float alpha = 0.0;\n  float cap = 1.0;\n  float lift = 0.0;\n\n  uint panes = v_mask;\n\n  for (int i = 0; i < MAX_PANES; i++) {\n    if ((panes >> uint(i)) == 0u) break;\n    if ((panes & (1u << uint(i))) == 0u) continue;\n\n    vec4 rect = u_rect[i];\n    vec4 radii = u_radii[i];\n    vec4 extra = u_extra[i];\n    vec2 q = p - rect.xy;\n\n    if (extra.x * amp > 0.0) q += extra.x * amp * warp(p, t, radii.w);\n\n    vec3 sd = roundedBox(q, rect.zw, radii.x);\n    float k = radii.y;\n    float h = max(k - abs(d - sd.x), 0.0) / k;\n    float w = d < sd.x ? h * 0.5 : 1.0 - h * 0.5;\n\n    d = min(d, sd.x) - h * h * k * 0.25;\n    g = mix(g, sd.yz, w);\n    tint = mix(tint, u_tint[i], w);\n    frost = mix(frost, radii.z, w);\n    bevel = mix(bevel, extra.z, w);\n    flow = mix(flow, extra.x, w);\n    press = mix(press, extra.y, w);\n    alpha = mix(alpha, extra.w, w);\n    cap = mix(cap, u_face[i].x, w);\n    lift = mix(lift, u_face[i].y, w);\n  }\n\n  if (d > SHADOW_REACH) {\n    fragColor = vec4(0.0);\n    return;\n  }\n\n  float shadow = u_look.x * alpha * (0.6 * softStep(d - 8.0 * g.y, 14.0) + 0.4 * softStep(d, 3.0));\n  float cover = clamp(0.5 - d * u_look.w, 0.0, 1.0);\n\n  if (cover <= 0.0) {\n    fragColor = castShadow(p, shadow, tint);\n    return;\n  }\n\n  float s = -d;\n  float W = max(bevel, 1.0);\n  float u = s / W;\n  float v = 1.0 - max(u, 0.002);\n  float v4 = v * v * v * v;\n  bool inBevel = u < 1.0;\n  float f = inBevel ? pow(1.0 - v4, 0.25) : 1.0;\n  float fp = inBevel ? v * v * v * pow(max(1.0 - v4, 1e-6), -0.75) : 0.0;\n  float T = u_optics.y;\n  float rise = 0.3 + 0.7 * f;\n  float z = T * rise;\n  float slope = min(0.7 * T / W * fp, 12.0);\n\n  float gradLength = length(g);\n  vec2 n2 = gradLength > 1e-4 ? g / gradLength : vec2(0.0, -1.0);\n  vec2 tilt = slope * n2;\n\n  if (flow * amp > 0.0) tilt += flow * amp * 0.18 * ripple(p, t);\n\n  vec3 N = normalize(vec3(tilt, 1.0));\n  float ior = max(u_optics.x, 1.0);\n  vec4 ray = through(N, ior, z);\n  vec2 base = p + ray.xy;\n\n  float R = frost * rise;\n  float lod = max(log2(max(R * u_density * 0.5, 1.0)), u_baseLod);\n\n  // Violet and red land at the ends of a short line; the spectrum between\n  // them only shows once that line spans half a device pixel.\n  float spread = 0.4 * u_optics.z;\n  vec2 violet = base;\n  vec2 span = vec2(0.0);\n\n  if (spread > 0.0 && dot(tilt, tilt) > 1e-8) {\n    violet = p + through(N, ior + spread * VIOLET, z).xy;\n    span = p + through(N, max(ior + spread * RED, 1.0), z).xy - violet;\n  }\n\n  float spanLength = length(span);\n  bool split = spanLength * u_look.w >= 0.5;\n  float texels = spanLength * u_density;\n  float lodSplit = max(lod, log2(max(texels * 0.14, 1.0)));\n  bool sharp = R < 0.5 && lodSplit <= u_baseLod;\n  vec3 col = vec3(0.0);\n\n  if (R >= 0.5) {\n    for (int i = 0; i < 9; i++) col += sampleAt(base + DISK[i] * R, lod);\n    col /= 9.0;\n  } else if (!split || !sharp) {\n    col = sampleAt(base, u_baseLod);\n  }\n\n  if (split) {\n    vec3 bands = vec3(0.0);\n\n    if (texels > 3.0) {\n      for (int k = 0; k < 7; k++) bands += BAND_RGB[k] * sampleAt(violet + span * BAND_AT[k], lodSplit);\n    } else {\n      bands = vec3(\n        sampleAt(violet + span * CENTROID.r, lodSplit).r,\n        sampleAt(violet + span * CENTROID.g, lodSplit).g,\n        sampleAt(violet + span * CENTROID.b, lodSplit).b\n      );\n    }\n\n    // Sharp glass shows the spectrum as it is; frosted or widely split glass\n    // keeps its own detail and takes on the colour the spectrum adds.\n    col = sharp ? bands : col + bands - sampleAt(base, lodSplit);\n  }\n\n  float luma = dot(col, LUMA);\n  col = mix(vec3(luma), col, 1.25);\n\n  float cosIn = sqrt(max(1.0 - (1.0 - N.z * N.z) / (ior * ior), 0.0));\n  float head = (ior - 1.0) / (ior + 1.0);\n  head *= head;\n  float reflectance = clamp((fresnel(1.0, ior, N.z, cosIn) - head) / (1.0 - head) * u_optics.w, 0.0, 1.0);\n  vec3 sky = vec3(1.0);\n\n  if (ray.w < 0.996 || reflectance > 0.004) {\n    sky = mix(sampleAt(p + n2 * 18.0, log2(max(u_density, 1.0)) + 3.5), vec3(1.0), 0.3);\n  }\n\n  if (ray.w < 0.996) col = mix(sky, col, ray.w);\n\n  float depth = rise * ray.z;\n  float passed = exp2(log2(max(1.0 - tint.a, 1e-4)) * depth);\n  col = col * passed + tint.rgb / max(tint.a, 1e-4) * (1.0 - passed);\n\n  if (reflectance > 0.004) {\n    vec3 mirrored = vec3(2.0 * N.z * N.xy, 2.0 * N.z * N.z - 1.0);\n    float up = smoothstep(-0.3, 0.1, mirrored.z);\n    vec3 env = sky;\n\n    if (up < 1.0) {\n      float reach = min((z + AIR_GAP) / max(-mirrored.z, 1e-3), 200.0);\n      env = mix(sampleAt(p + mirrored.xy * reach, log2(max(u_density, 1.0)) + 1.5), sky, up);\n    }\n\n    col = mix(col, env, reflectance);\n  }\n\n  float facing = pow(abs(dot(n2, normalize(LIGHT.xy))), 1.5);\n  col += exp(-max(s, 0.0) / 0.9) * 0.34 * (0.3 + 0.7 * facing) * u_look.y;\n\n  // A pressed pane brightens as a whole, wherever it was touched.\n  col = mix(col, vec3(1.0), 0.08 * press);\n\n  col = clamp(col, 0.0, 1.0);\n\n  if (cap < 1.0 || lift > 0.0) col = mix(col, legible(col, cap, lift), smoothstep(0.5, 2.0, s));\n\n  float a = cover * alpha;\n  vec4 under = a < 0.999 ? castShadow(p, shadow, tint) : vec4(0.0);\n  fragColor = vec4(col * a + (1.0 - a) * under.rgb, a + (1.0 - a) * under.a);\n}`;\n\nconst UNIFORMS = [\n  'u_backdrop',\n  'u_size',\n  'u_backing',\n  'u_texMap',\n  'u_baseLod',\n  'u_density',\n  'u_rect',\n  'u_radii',\n  'u_tint',\n  'u_extra',\n  'u_face',\n  'u_optics',\n  'u_look',\n  'u_time',\n] as const;\n\ntype Uniforms = Record<(typeof UNIFORMS)[number], WebGLUniformLocation | null>;\n\ntype Gpu = {\n  program: WebGLProgram;\n  vao: WebGLVertexArrayObject;\n  cells: WebGLBuffer;\n  texture: WebGLTexture;\n  uniforms: Uniforms;\n  maxTexture: number;\n};\n\n// Offsets into the packed frame. Every value a draw depends on lives here, so\n// comparing two frames tells whether anything on screen would change.\nconst RECT = 0;\n\nconst RADII = 96;\n\nconst TINT = 192;\n\nconst EXTRA = 288;\n\nconst OPTICS = 384;\n\nconst LOOK = 388;\n\nconst TEX_MAP = 392;\n\nconst BASE_LOD = 396;\n\nconst DENSITY = 397;\n\nconst TIME = 398;\n\nconst COUNT = 399;\n\nconst SIZE = 400;\n\nconst BACKING = 402;\n\nconst FACE = 404;\n\nconst FRAME_LENGTH = 452;\n\ntype Rgba = [number, number, number, number];\n\ntype Pane = {\n  element: HTMLElement;\n  seed: number;\n  cx: number;\n  cy: number;\n  hx: number;\n  hy: number;\n  radius: number;\n  /** Premultiplied. */\n  tint: Rgba;\n  frost: number;\n  merge: number;\n  flow: number;\n  /** Brightest and darkest the face may get while its ink stays legible. */\n  cap: number;\n  lift: number;\n  opacity: number;\n  presence: Spring;\n  press: Spring;\n  /** Still fading out after its element was hidden or removed. */\n  ghost: boolean;\n};\n\nconst TRANSPARENT: Rgba = [0, 0, 0, 0];\n\nconst NUMBER = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?$/i;\n\nconst clamp01 = (value: number) => Math.min(Math.max(value, 0), 1);\n\nconst channel = (token: string, scale: number) =>\n  token === 'none' ? 0 : token.endsWith('%') ? (Number(token.slice(0, -1)) / 100) * scale : Number(token);\n\nconst hexColour = (hex: string): Rgba | null => {\n  const digits = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join('') : hex;\n\n  if (!/^[\\da-f]{6}([\\da-f]{2})?$/i.test(digits)) return null;\n\n  const value = (index: number) => parseInt(digits.slice(index, index + 2), 16) / 255;\n\n  return [value(0), value(2), value(4), digits.length === 8 ? value(6) : 1];\n};\n\n// Reads the sRGB colours browsers serialise for registered <color> properties\n// without touching a canvas.\nconst quickColour = (text: string): Rgba | null => {\n  if (text.startsWith('#')) return hexColour(text.slice(1));\n\n  const match = /^(rgba?|color)\\((.*)\\)$/i.exec(text);\n\n  if (!match) return null;\n\n  const tokens = match[2].split(/[\\s,/]+/).filter(Boolean);\n  const isColourFunction = match[1].toLowerCase() === 'color';\n\n  if (isColourFunction && tokens.shift()?.toLowerCase() !== 'srgb') return null;\n\n  if (tokens.length < 3 || tokens.length > 4) return null;\n\n  if (!tokens.every((token) => token === 'none' || NUMBER.test(token.replace(/%$/, '')))) return null;\n\n  const scale = isColourFunction ? 1 : 255;\n\n  return [\n    channel(tokens[0], scale) / scale,\n    channel(tokens[1], scale) / scale,\n    channel(tokens[2], scale) / scale,\n    tokens[3] === undefined ? 1 : channel(tokens[3], 1),\n  ];\n};\n\nconst colourCache = new Map<string, Rgba>();\n\nlet colourProbe: CanvasRenderingContext2D | null = null;\n\n// Any other colour syntax is painted into one pixel and read back.\nconst probeColour = (text: string): Rgba | null => {\n  if (!CSS.supports('color', text)) return null;\n\n  colourProbe ??= (() => {\n    const probe = document.createElement('canvas');\n    probe.width = 1;\n    probe.height = 1;\n\n    return probe.getContext('2d', { willReadFrequently: true });\n  })();\n\n  if (!colourProbe) return null;\n\n  colourProbe.clearRect(0, 0, 1, 1);\n  colourProbe.fillStyle = text;\n  colourProbe.fillRect(0, 0, 1, 1);\n  const [red, green, blue, alpha] = colourProbe.getImageData(0, 0, 1, 1).data;\n\n  return [red / 255, green / 255, blue / 255, alpha / 255];\n};\n\nconst premultiply = ([red, green, blue, alpha]: Rgba): Rgba => {\n  const a = clamp01(alpha);\n\n  return [clamp01(red) * a, clamp01(green) * a, clamp01(blue) * a, a];\n};\n\nconst parseColour = (value: string): Rgba => {\n  const text = value.trim();\n\n  if (!text || text === 'transparent') return TRANSPARENT;\n\n  const quick = quickColour(text);\n\n  if (quick && quick.every(Number.isFinite)) return premultiply(quick);\n\n  const cached = colourCache.get(text);\n\n  if (cached) return cached;\n\n  const probed = probeColour(text);\n\n  if (!probed) {\n    warnOnce(`colour:${text}`, `\"${text}\" is not a colour, so the glass ignores it.`);\n\n    return TRANSPARENT;\n  }\n\n  if (colourCache.size > 256) colourCache.clear();\n\n  const colour = premultiply(probed);\n  colourCache.set(text, colour);\n\n  return colour;\n};\n\nconst toLinear = (channel: number) =>\n  channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;\n\n// The face luminance range that keeps text in this premultiplied colour\n// legible. Ink brighter than 0.18, where the two bounds meet, caps how bright\n// the face may get; darker ink sets how bright it must stay. Transparent ink\n// leaves the glass as it is.\nconst faceRange = ([red, green, blue, alpha]: Rgba): [number, number] => {\n  if (alpha <= 0) return [1, 0];\n\n  const ink =\n    0.2126 * toLinear(red / alpha) + 0.7152 * toLinear(green / alpha) + 0.0722 * toLinear(blue / alpha);\n\n  return ink >= 0.18\n    ? [clamp01((ink + 0.05) / INK_CONTRAST - 0.05), 0]\n    : [1, clamp01(INK_CONTRAST * (ink + 0.05) - 0.05)];\n};\n\n// Finds the first url() in a computed background-image and undoes CSS escapes.\nconst firstUrl = (value: string) => {\n  const match = /url\\(\\s*(?:\"((?:[^\"\\\\]|\\\\[\\s\\S])*)\"|'((?:[^'\\\\]|\\\\[\\s\\S])*)'|([^)\\s]*))\\s*\\)/i.exec(value);\n\n  if (!match) return null;\n\n  const raw = match[1] ?? match[2] ?? match[3] ?? '';\n\n  const url = raw.replace(/\\\\([\\da-f]{1,6}\\s?|[\\s\\S])/gi, (_, escape: string) =>\n    /^[\\da-f]/i.test(escape) ? String.fromCodePoint(parseInt(escape, 16)) : escape,\n  );\n\n  return url || null;\n};\n\nconst describeUrl = (url: string) => (url.length > 80 ? `${url.slice(0, 77)}...` : url);\n\n// WebGL reads same-origin, data: and blob: images without CORS.\nconst needsCors = (url: string) => {\n  const target = new URL(url, document.baseURI);\n\n  return /^https?:$/.test(target.protocol) && target.origin !== window.origin;\n};\n\nexport function createSceneRenderer({\n  root,\n  canvas,\n  backdrop,\n  registry,\n  settings: initialSettings,\n}: SceneRendererOptions): SceneRenderer {\n  const setAttribute = (name: string, value: string) => {\n    if (root.getAttribute(name) !== value) root.setAttribute(name, value);\n  };\n\n  const context = canvas.getContext('webgl2', {\n    alpha: true,\n    premultipliedAlpha: true,\n    antialias: false,\n    depth: false,\n    stencil: false,\n    preserveDrawingBuffer: false,\n  });\n\n  if (!context) {\n    warnOnce('webgl2', 'WebGL2 is unavailable, so the scene keeps its CSS glass.');\n    setAttribute(RENDERER, 'css');\n\n    return { update: () => {}, destroy: () => {} };\n  }\n\n  const gl = context;\n  let settings = initialSettings;\n  let gpu: Gpu | null = null;\n  let lost = gl.isContextLost();\n  let failed = false;\n  let destroyed = false;\n  // True once the canvas has drawn the scene and until it hands back to CSS.\n  let live = false;\n  let mode: 'on' | 'off' = 'on';\n  let reducedMotion = false;\n  let lowQuality = false;\n  let hidden = document.hidden;\n  let onscreen = true;\n  let frameId = 0;\n  let lastTick = 0;\n  let tailUntil = 0;\n  let flowTime = 0;\n  let flowing = false;\n  let snapPresence = true;\n  let dirty = true;\n  let seedCounter = 0;\n  let scaleIndex = 0;\n  let shadowStrength = 0;\n  let highlightStrength = 0;\n  const deltas: number[] = [];\n\n  const box = { left: 0, top: 0, width: 0, height: 0 };\n  const records = new Map<HTMLElement, Pane>();\n  let drawn: Pane[] = [];\n  const frame = new Float32Array(FRAME_LENGTH);\n  const previous = new Float32Array(FRAME_LENGTH);\n  // Cell vertices, reused between frames: a corner as two floats, then a mask.\n  let cellFloats = new Float32Array(0);\n  let cellMasks = new Uint32Array(0);\n\n  // Backdrop image, from the CSS value to the texture on the GPU.\n  let requestedUrl: string | null | undefined;\n  let loadGeneration = 0;\n  let image: HTMLImageElement | null = null;\n  let imageUrl: string | null = null;\n  let fill = 'rgb(255, 255, 255)';\n\n  // The backdrop image lands at left/top in the raster, stepX/stepY raster\n  // pixels per image pixel.\n  let raster: {\n    canvas: HTMLCanvasElement;\n    key: string;\n    url: string;\n    width: number;\n    height: number;\n    left: number;\n    top: number;\n    stepX: number;\n    stepY: number;\n  } | null = null;\n\n  let rasterAt = 0;\n  let uploaded = false;\n\n  const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');\n  const contrastQuery = window.matchMedia('(prefers-contrast: more)');\n  const forcedQuery = window.matchMedia('(forced-colors: active)');\n  let resolutionQuery: MediaQueryList | null = null;\n\n  // Values that fall outside the quick paths (calc() in engines without\n  // @property) are resolved by a hidden element inside the backdrop layer.\n  let probe: HTMLSpanElement | null = null;\n  const probeCache = new Map<string, number>();\n\n  const resolve = (raw: string, property: 'width' | 'flexGrow') => {\n    const key = `${property}:${raw}`;\n    const cached = probeCache.get(key);\n\n    if (cached !== undefined) return cached;\n\n    if (!probe) {\n      probe = document.createElement('span');\n      probe.style.cssText = 'position:absolute;visibility:hidden;height:0;';\n      backdrop.append(probe);\n    }\n\n    probe.style[property] = raw;\n    const value = parseFloat(getComputedStyle(probe)[property]);\n    probe.style[property] = '';\n    const result = Number.isFinite(value) ? value : 0;\n\n    if (probeCache.size > 256) probeCache.clear();\n\n    probeCache.set(key, result);\n\n    return result;\n  };\n\n  const readLength = (style: CSSStyleDeclaration, name: string) => {\n    const raw = style.getPropertyValue(name).trim();\n\n    if (!raw) return 0;\n\n    if (raw.endsWith('px') && NUMBER.test(raw.slice(0, -2))) return Number(raw.slice(0, -2));\n\n    if (NUMBER.test(raw)) return Number(raw);\n\n    return resolve(raw, 'width');\n  };\n\n  const readNumber = (style: CSSStyleDeclaration, name: string) => {\n    const raw = style.getPropertyValue(name).trim();\n\n    if (!raw) return 0;\n\n    return NUMBER.test(raw) ? Number(raw) : resolve(raw, 'flexGrow');\n  };\n\n  const measureBox = () => {\n    const rect = root.getBoundingClientRect();\n    const scaleX = root.offsetWidth ? rect.width / root.offsetWidth : 1;\n    const scaleY = root.offsetHeight ? rect.height / root.offsetHeight : 1;\n\n    box.left = rect.left + root.clientLeft * scaleX;\n    box.top = rect.top + root.clientTop * scaleY;\n    box.width = Math.max(rect.width - (root.offsetWidth - root.clientWidth) * scaleX, 0);\n    box.height = Math.max(rect.height - (root.offsetHeight - root.clientHeight) * scaleY, 0);\n  };\n\n  // Glass nested inside other registered glass keeps its CSS material: one\n  // field cannot show glass on top of glass.\n  const eligible = (element: HTMLElement) => {\n    if (element === root || !element.isConnected || !root.contains(element)) return false;\n\n    for (let node = element.parentElement; node && node !== root; node = node.parentElement) {\n      if (registry.elements.has(node)) return false;\n    }\n\n    return true;\n  };\n\n  const opacityOf = (element: HTMLElement, style: CSSStyleDeclaration) => {\n    let opacity = Number(style.opacity);\n\n    for (let node = element.parentElement; node && node !== root; node = node.parentElement) {\n      opacity *= Number(getComputedStyle(node).opacity);\n    }\n\n    return Number.isFinite(opacity) ? clamp01(opacity) : 1;\n  };\n\n  const radiusOf = (style: CSSStyleDeclaration, width: number, height: number) => {\n    const token = style.borderTopLeftRadius.split(' ')[0] ?? '0';\n    const value = parseFloat(token);\n\n    if (!Number.isFinite(value)) return 0;\n\n    return token.endsWith('%') ? (value / 100) * Math.min(width, height) : value;\n  };\n\n  const measure = (pane: Pane, style: CSSStyleDeclaration) => {\n    const { element } = pane;\n    const rect = element.getBoundingClientRect();\n    const scale = element.offsetWidth ? rect.width / element.offsetWidth : 1;\n\n    pane.cx = rect.left + rect.width / 2 - box.left;\n    pane.cy = rect.top + rect.height / 2 - box.top;\n    pane.hx = rect.width / 2;\n    pane.hy = rect.height / 2;\n    pane.radius = Math.min(\n      radiusOf(style, element.offsetWidth, element.offsetHeight) * scale,\n      pane.hx,\n      pane.hy,\n    );\n    pane.tint = parseColour(style.getPropertyValue('--lg-scene-tint'));\n    pane.frost = readLength(style, '--lg-scene-frost');\n    pane.merge = readLength(style, '--lg-scene-merge');\n    pane.flow = readNumber(style, '--lg-scene-flow');\n    [pane.cap, pane.lift] = faceRange(parseColour(style.getPropertyValue('--lg-scene-ink')));\n    pane.press.target = clamp01(readNumber(style, '--lg-press'));\n    pane.opacity = opacityOf(element, style);\n  };\n\n  const release = (pane: Pane) => {\n    pane.element.removeAttribute(ACTIVE);\n    records.delete(pane.element);\n    resizeObserver.unobserve(pane.element);\n  };\n\n  // Hands every element back to its CSS glass.\n  const deactivate = () => {\n    for (const pane of records.values()) release(pane);\n\n    live = false;\n    drawn = [];\n    snapPresence = true;\n    dirty = true;\n  };\n\n  const stop = () => {\n    if (frameId) cancelAnimationFrame(frameId);\n\n    frameId = 0;\n    lastTick = 0;\n  };\n\n  const canDraw = () => !destroyed && !lost && !failed && mode === 'on' && gpu !== null;\n\n  const fail = (key: string, message: string) => {\n    deactivate();\n    image = null;\n    imageUrl = null;\n    raster = null;\n    uploaded = false;\n    setAttribute(RENDERER, 'css');\n    setAttribute(BACKDROP, 'error');\n    warnOnce(key, message);\n  };\n\n  /*\n   * The renderer loads the backdrop as the layer does, so the browser\n   * downloads a same-origin image once. Only another origin needs a CORS copy\n   * for WebGL; asking for it after the shared load lets the browser answer\n   * from cache when the response allows any origin.\n   */\n  const load = (url: string) => {\n    loadGeneration += 1;\n    const generation = loadGeneration;\n\n    const request = (cors: boolean, onLoad: (next: HTMLImageElement) => void) => {\n      const next = new Image();\n\n      if (cors) next.crossOrigin = 'anonymous';\n\n      next.decoding = 'async';\n      next.addEventListener('load', () => {\n        if (generation === loadGeneration && !destroyed) onLoad(next);\n      });\n      next.addEventListener('error', () => {\n        if (generation !== loadGeneration || destroyed) return;\n\n        fail(\n          'backdrop',\n          cors\n            ? `the backdrop image ${describeUrl(url)} failed to load with CORS, so WebGL cannot read it and the scene keeps its CSS glass. Serve it from this origin or with CORS headers.`\n            : `the backdrop image ${describeUrl(url)} failed to load, so the scene keeps its CSS glass.`,\n        );\n      });\n      next.src = url;\n    };\n\n    const accept = (next: HTMLImageElement) => {\n      if (!next.naturalWidth || !next.naturalHeight) {\n        fail('backdrop', `the backdrop image ${describeUrl(url)} has no size, so the scene keeps its CSS glass.`);\n\n        return;\n      }\n\n      image = next;\n      imageUrl = url;\n      dirty = true;\n      wake();\n    };\n\n    setAttribute(BACKDROP, 'loading');\n    request(false, (shared) => {\n      if (needsCors(url)) request(true, accept);\n      else accept(shared);\n    });\n  };\n\n  const checkBackdrop = () => {\n    // After React removes a scene and before it destroys the renderer, a\n    // navigation's head update still wakes the observers. The detached layer\n    // has no computed style, which is not a missing backdrop.\n    if (!backdrop.isConnected) return;\n\n    const url = firstUrl(getComputedStyle(backdrop).backgroundImage);\n\n    if (url === requestedUrl) return;\n\n    requestedUrl = url;\n\n    if (url) load(url);\n    else {\n      loadGeneration += 1;\n      fail('backdrop', 'the scene has no backdrop image, so it keeps its CSS glass.');\n    }\n  };\n\n  // The raster fills transparent parts of the backdrop with the colour behind\n  // the scene, as the page would.\n  const readFill = () => {\n    for (let node: HTMLElement | null = root; node; node = node.parentElement) {\n      const colour = getComputedStyle(node).backgroundColor;\n      const rgba = parseColour(colour);\n\n      if (rgba[3] > 0) return colour;\n    }\n\n    return 'rgb(255, 255, 255)';\n  };\n\n  const readLook = () => {\n    const style = getComputedStyle(root);\n    const shadow = Number(style.getPropertyValue('--lg-scene-shadow'));\n    const highlight = Number(style.getPropertyValue('--lg-scene-highlight'));\n\n    shadowStrength = Number.isFinite(shadow) ? shadow : 0;\n    highlightStrength = Number.isFinite(highlight) ? highlight : 0;\n  };\n\n  const coverFit = (width: number, height: number, source: HTMLImageElement) => {\n    const scale = Math.max(width / source.naturalWidth, height / source.naturalHeight);\n\n    return {\n      scale,\n      offsetX: (width - source.naturalWidth * scale) / 2,\n      offsetY: (height - source.naturalHeight * scale) / 2,\n    };\n  };\n\n  const upload = () => {\n    if (!gpu || !raster) return false;\n\n    gl.bindTexture(gl.TEXTURE_2D, gpu.texture);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);\n\n    try {\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, raster.canvas);\n    } catch (error) {\n      fail(\n        'backdrop',\n        `the backdrop image ${describeUrl(raster.url)} cannot be read by WebGL (${\n          error instanceof Error ? error.message : String(error)\n        }). Serve it from this origin or with CORS headers.`,\n      );\n\n      return false;\n    }\n\n    gl.generateMipmap(gl.TEXTURE_2D);\n    uploaded = true;\n    dirty = true;\n\n    return true;\n  };\n\n  // Draws the backdrop exactly as CSS paints it (cover, centred) into a\n  // canvas at up to 2x resolution, then uploads it with mipmaps for frost.\n  const ensureRaster = (force: boolean) => {\n    if (!gpu || !image || !imageUrl) return false;\n\n    const width = Math.max(box.width, 1);\n    const height = Math.max(box.height, 1);\n\n    const density = Math.min(\n      window.devicePixelRatio || 1,\n      lowQuality ? 1 : 2,\n      MAX_TEXTURE / width,\n      MAX_TEXTURE / height,\n      gpu.maxTexture / width,\n      gpu.maxTexture / height,\n    );\n\n    const textureWidth = Math.max(1, Math.ceil(width * density));\n    const textureHeight = Math.max(1, Math.ceil(height * density));\n    const key = `${textureWidth}x${textureHeight}@${density}|${fill}|${imageUrl}`;\n\n    if (raster?.key === key) return uploaded || upload();\n\n    const now = performance.now();\n\n    if (raster && raster.url === imageUrl && uploaded && !force && now - rasterAt < RASTER_THROTTLE_MS) return true;\n\n    const target = raster?.canvas ?? document.createElement('canvas');\n    const context2d = target.getContext('2d');\n\n    if (!context2d) {\n      fail('raster', 'a 2D canvas is unavailable, so the scene keeps its CSS glass.');\n\n      return false;\n    }\n\n    // The image's edges snap to whole raster pixels, as CSS snaps a\n    // background. Firefox resamples an SVG drawn at a fractional size, which\n    // would blur the glass by half a pixel.\n    const fit = coverFit(width, height, image);\n    const left = Math.round(fit.offsetX * density);\n    const top = Math.round(fit.offsetY * density);\n    const drawnWidth = Math.round((fit.offsetX + image.naturalWidth * fit.scale) * density) - left;\n    const drawnHeight = Math.round((fit.offsetY + image.naturalHeight * fit.scale) * density) - top;\n\n    target.width = textureWidth;\n    target.height = textureHeight;\n    context2d.setTransform(1, 0, 0, 1, 0, 0);\n    context2d.fillStyle = fill;\n    context2d.fillRect(0, 0, textureWidth, textureHeight);\n    context2d.drawImage(image, left, top, drawnWidth, drawnHeight);\n\n    raster = {\n      canvas: target,\n      key,\n      url: imageUrl,\n      width: textureWidth,\n      height: textureHeight,\n      left,\n      top,\n      stepX: drawnWidth / image.naturalWidth,\n      stepY: drawnHeight / image.naturalHeight,\n    };\n    rasterAt = now;\n    uploaded = false;\n\n    return upload();\n  };\n\n  const createPane = (element: HTMLElement): Pane => {\n    seedCounter += 1;\n\n    return {\n      element,\n      seed: seedCounter * 1.618,\n      cx: 0,\n      cy: 0,\n      hx: 0,\n      hy: 0,\n      radius: 0,\n      tint: TRANSPARENT,\n      frost: 0,\n      merge: 0,\n      flow: 0,\n      cap: 1,\n      lift: 0,\n      opacity: 1,\n      presence: createSpring(0.35, 0.3, snapPresence ? 1 : 0),\n      press: createSpring(0.2, 0.3, 0),\n      ghost: false,\n    };\n  };\n\n  // Reads the DOM into pane records and advances every spring. Returns\n  // whether anything is still moving.\n  const sync = (dt: number) => {\n    measureBox();\n    checkBackdrop();\n    readLook();\n\n    const seen = new Set<Pane>();\n\n    for (const element of registry.elements) {\n      if (seen.size >= MAX_PANES) break;\n\n      if (!eligible(element)) continue;\n\n      const style = getComputedStyle(element);\n\n      if (style.visibility !== 'visible' || element.getClientRects().length === 0) continue;\n\n      let pane = records.get(element);\n\n      if (!pane) {\n        pane = createPane(element);\n        records.set(element, pane);\n        resizeObserver.observe(element);\n      }\n\n      measure(pane, style);\n\n      if (pane.hx <= 0 || pane.hy <= 0) continue;\n\n      pane.ghost = false;\n      pane.presence.target = 1;\n      seen.add(pane);\n    }\n\n    for (const pane of records.values()) {\n      if (seen.has(pane)) continue;\n\n      const { element } = pane;\n\n      // An element that unmounted or hid itself dissolves; one that left the\n      // scene or the registry returns to CSS at once.\n      const dissolves =\n        !reducedMotion && pane.hx > 0 && (!element.isConnected || (registry.elements.has(element) && eligible(element)));\n\n      if (!dissolves) {\n        release(pane);\n        continue;\n      }\n\n      pane.ghost = true;\n      pane.presence.target = 0;\n    }\n\n    let moving = false;\n\n    for (const pane of records.values()) {\n      moving = stepSpring(pane.presence, dt, reducedMotion) || moving;\n      moving = stepSpring(pane.press, dt, reducedMotion) || moving;\n\n      if (pane.ghost && pane.presence.value < CULL) release(pane);\n    }\n\n    flowing =\n      settings.flow &&\n      !reducedMotion &&\n      !lowQuality &&\n      [...records.values()].some((pane) => pane.flow > 0 && pane.presence.value >= CULL);\n\n    return moving;\n  };\n\n  // Panes fold in order of decreasing merge radius, so a wide merge never\n  // swallows a narrower one folded before it. Live panes claim slots first.\n  const pack = (ratio: number, backingWidth: number, backingHeight: number) => {\n    const live = [...records.values()].filter((pane) => !pane.ghost);\n\n    const ghosts = [...records.values()]\n      .filter((pane) => pane.ghost)\n      .sort((a, b) => b.presence.value - a.presence.value);\n\n    drawn = [...live, ...ghosts]\n      .slice(0, MAX_PANES)\n      .filter((pane) => pane.presence.value >= CULL)\n      .sort((a, b) => b.merge - a.merge);\n\n    frame.fill(0);\n\n    const { optics } = settings;\n\n    drawn.forEach((pane, index) => {\n      const presence = pane.presence.value;\n      const scale = 0.6 + 0.4 * presence;\n      const halfX = pane.hx * scale;\n      const halfY = pane.hy * scale;\n      const bevel = Math.max(Math.min(optics.bevel, pane.radius, pane.hx, pane.hy), 1) * scale;\n\n      frame.set([pane.cx, pane.cy, halfX, halfY], RECT + index * 4);\n      frame.set([pane.radius * scale, Math.max(pane.merge, 1e-4), pane.frost * optics.frost, pane.seed], RADII + index * 4);\n      frame.set(pane.tint, TINT + index * 4);\n      frame.set([pane.flow, pane.press.value, bevel, clamp01(presence) * pane.opacity], EXTRA + index * 4);\n      frame.set([pane.cap, pane.lift], FACE + index * 2);\n    });\n\n    frame.set([optics.ior, optics.thickness, optics.dispersion, optics.specular], OPTICS);\n    frame[TIME] = flowing ? flowTime : 0;\n    frame.set([shadowStrength, highlightStrength, flowing ? 1 : 0, ratio], LOOK);\n\n    if (raster && image) {\n      /*\n       * The raster may lag a resize by a few frames. Mapping through the\n       * backdrop's image pixels keeps it aligned meanwhile: box point q shows\n       * image pixel (q - o) / s now, which sits at raster pixel\n       * left + (q - o) step / s.\n       */\n      const fit = coverFit(Math.max(box.width, 1), Math.max(box.height, 1), image);\n      const densityX = raster.stepX / fit.scale;\n      const densityY = raster.stepY / fit.scale;\n\n      frame.set(\n        [\n          densityX / raster.width,\n          densityY / raster.height,\n          (raster.left - fit.offsetX * densityX) / raster.width,\n          (raster.top - fit.offsetY * densityY) / raster.height,\n        ],\n        TEX_MAP,\n      );\n      frame[BASE_LOD] = Math.max(Math.log2(densityX / ratio), 0);\n      frame[DENSITY] = densityX;\n    }\n\n    frame[COUNT] = drawn.length;\n    frame.set([box.width, box.height], SIZE);\n    frame.set([backingWidth, backingHeight], BACKING);\n  };\n\n  /*\n   * Splits the canvas into the cells that some glass can reach and writes two\n   * triangles per cell, each vertex carrying the mask of panes that reach the\n   * cell. A pane reaches its shadow's extent, twice the widest merge radius it\n   * can fold with (past which the smooth minimum leaves visible pixels\n   * unchanged) and the sway of a flowing edge. Every edge of those areas cuts\n   * the canvas into columns and rows; a cell's mask is its column's mask and\n   * its row's, and equal neighbours along a row join into one quad. Returns\n   * the number of vertices.\n   */\n  const buildCells = (backingWidth: number, backingHeight: number) => {\n    const ratioX = backingWidth / frame[SIZE];\n    const ratioY = backingHeight / frame[SIZE + 1];\n    const areas: number[] = [];\n\n    const reach = (bit: number, cx: number, cy: number, halfX: number, halfY: number) => {\n      const x0 = Math.max(Math.floor((cx - halfX) * ratioX), 0);\n      const x1 = Math.min(Math.ceil((cx + halfX) * ratioX), backingWidth);\n      const y0 = Math.max(Math.floor((cy - halfY) * ratioY), 0);\n      const y1 = Math.min(Math.ceil((cy + halfY) * ratioY), backingHeight);\n\n      if (x1 > x0 && y1 > y0) areas.push(x0, x1, y0, y1, 2 ** bit);\n    };\n\n    for (let index = 0; index < drawn.length; index += 1) {\n      const rect = RECT + index * 4;\n      const sway = frame[LOOK + 2] > 0 ? WOBBLE * frame[EXTRA + index * 4] : 0;\n      const grow = SHADOW_REACH + 1 + 2 * frame[RADII + index * 4 + 1] + sway;\n\n      reach(index, frame[rect], frame[rect + 1], frame[rect + 2] + grow, frame[rect + 3] + grow);\n    }\n\n    const edges = (offset: number) => {\n      const values = new Set<number>();\n\n      for (let index = offset; index < areas.length; index += 5) values.add(areas[index]).add(areas[index + 1]);\n\n      return [...values].sort((a, b) => a - b);\n    };\n\n    const columns = edges(0);\n    const rows = edges(2);\n\n    const masks = (lines: number[], offset: number) =>\n      lines.slice(0, -1).map((start, index) => {\n        let mask = 0;\n\n        for (let area = 0; area < areas.length; area += 5) {\n          if (areas[area + offset] <= start && areas[area + offset + 1] >= lines[index + 1]) mask |= areas[area + 4];\n        }\n\n        return mask;\n      });\n\n    const columnMasks = masks(columns, 0);\n    const rowMasks = masks(rows, 2);\n    const needed = columnMasks.length * rowMasks.length * 6 * 3;\n\n    if (cellFloats.length < needed) {\n      const data = new ArrayBuffer(Math.max(needed, cellFloats.length * 2) * 4);\n      cellFloats = new Float32Array(data);\n      cellMasks = new Uint32Array(data);\n    }\n\n    let vertices = 0;\n\n    const corner = (x: number, y: number, mask: number) => {\n      cellFloats[vertices * 3] = (x / backingWidth) * 2 - 1;\n      cellFloats[vertices * 3 + 1] = 1 - (y / backingHeight) * 2;\n      cellMasks[vertices * 3 + 2] = mask;\n      vertices += 1;\n    };\n\n    const quad = (x0: number, x1: number, y0: number, y1: number, mask: number) => {\n      corner(x0, y0, mask);\n      corner(x1, y0, mask);\n      corner(x0, y1, mask);\n      corner(x0, y1, mask);\n      corner(x1, y0, mask);\n      corner(x1, y1, mask);\n    };\n\n    rowMasks.forEach((rowMask, row) => {\n      let start = 0;\n      let open = 0;\n\n      columnMasks.forEach((columnMask, column) => {\n        const mask = columnMask & rowMask;\n\n        if (mask === open) return;\n\n        if (open) quad(columns[start], columns[column], rows[row], rows[row + 1], open);\n\n        start = column;\n        open = mask;\n      });\n\n      if (open) quad(columns[start], columns.at(-1) ?? 0, rows[row], rows[row + 1], open);\n    });\n\n    return vertices;\n  };\n\n  const sameFrame = () => {\n    for (let index = 0; index < FRAME_LENGTH; index += 1) {\n      if (frame[index] !== previous[index]) return false;\n    }\n\n    return true;\n  };\n\n  const render = (final: boolean) => {\n    if (!canDraw() || !gpu || !image || box.width < 1 || box.height < 1) return;\n\n    const scale = final ? 1 : SCALES[scaleIndex];\n    let ratio = Math.min(window.devicePixelRatio || 1, lowQuality ? 1 : 2) * scale;\n\n    if (box.width * box.height * ratio * ratio > MAX_PIXELS) ratio = Math.sqrt(MAX_PIXELS / (box.width * box.height));\n\n    const backingWidth = Math.max(1, Math.round(box.width * ratio));\n    const backingHeight = Math.max(1, Math.round(box.height * ratio));\n\n    if (canvas.width !== backingWidth || canvas.height !== backingHeight) {\n      canvas.width = backingWidth;\n      canvas.height = backingHeight;\n      dirty = true;\n    }\n\n    fill = readFill();\n\n    if (!ensureRaster(final) || !raster) return;\n\n    pack(backingWidth / box.width, backingWidth, backingHeight);\n\n    if (!dirty && sameFrame()) return;\n\n    const { uniforms } = gpu;\n    const vertices = buildCells(backingWidth, backingHeight);\n\n    gl.viewport(0, 0, backingWidth, backingHeight);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n\n    if (vertices > 0) {\n      gl.useProgram(gpu.program);\n      gl.bindVertexArray(gpu.vao);\n      gl.bindBuffer(gl.ARRAY_BUFFER, gpu.cells);\n      gl.bufferData(gl.ARRAY_BUFFER, cellFloats.subarray(0, vertices * 3), gl.DYNAMIC_DRAW);\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, gpu.texture);\n      gl.uniform1i(uniforms.u_backdrop, 0);\n      gl.uniform2f(uniforms.u_size, frame[SIZE], frame[SIZE + 1]);\n      gl.uniform2f(uniforms.u_backing, frame[BACKING], frame[BACKING + 1]);\n      gl.uniform4fv(uniforms.u_texMap, frame, TEX_MAP, 4);\n      gl.uniform1f(uniforms.u_baseLod, frame[BASE_LOD]);\n      gl.uniform1f(uniforms.u_density, frame[DENSITY]);\n      gl.uniform4fv(uniforms.u_rect, frame, RECT, MAX_PANES * 4);\n      gl.uniform4fv(uniforms.u_radii, frame, RADII, MAX_PANES * 4);\n      gl.uniform4fv(uniforms.u_tint, frame, TINT, MAX_PANES * 4);\n      gl.uniform4fv(uniforms.u_extra, frame, EXTRA, MAX_PANES * 4);\n      gl.uniform2fv(uniforms.u_face, frame, FACE, MAX_PANES * 2);\n      gl.uniform4fv(uniforms.u_optics, frame, OPTICS, 4);\n      gl.uniform4fv(uniforms.u_look, frame, LOOK, 4);\n      gl.uniform1f(uniforms.u_time, frame[TIME]);\n      gl.drawArrays(gl.TRIANGLES, 0, vertices);\n    }\n\n    previous.set(frame);\n    dirty = false;\n    snapPresence = false;\n\n    // The CSS material steps aside in the same frame the canvas takes over.\n    for (const pane of drawn) {\n      if (!pane.ghost && pane.element.getAttribute(ACTIVE) !== 'active') pane.element.setAttribute(ACTIVE, 'active');\n    }\n\n    live = true;\n    setAttribute(RENDERER, 'webgl');\n\n    if (raster.url === requestedUrl) setAttribute(BACKDROP, 'ready');\n  };\n\n  /*\n   * Glass that appears in a live scene hands over its material at once and\n   * grows in on the canvas, instead of flashing its CSS glass for a frame.\n   * Registration and mutation callbacks both run before the next paint.\n   */\n  const claim = () => {\n    if (!live || !canDraw()) return;\n\n    let count = records.size;\n\n    for (const element of registry.elements) {\n      if (count >= MAX_PANES) break;\n\n      if (records.has(element) || !eligible(element)) continue;\n\n      if (getComputedStyle(element).visibility !== 'visible' || element.getClientRects().length === 0) continue;\n\n      records.set(element, createPane(element));\n      resizeObserver.observe(element);\n      element.setAttribute(ACTIVE, 'active');\n      count += 1;\n    }\n  };\n\n  const animationsRunning = () => {\n    const checked = new Set<Element>();\n\n    for (const pane of records.values()) {\n      for (let node: Element | null = pane.element; node && !checked.has(node); node = node.parentElement) {\n        checked.add(node);\n\n        if (node.getAnimations().some((animation) => animation.playState === 'running')) return true;\n\n        if (node === root) break;\n      }\n    }\n\n    return false;\n  };\n\n  // Frame times steer the resolution: a slow median steps down, a fast one\n  // steps back up.\n  const adaptScale = (delta: number) => {\n    deltas.push(delta);\n\n    if (deltas.length > 24) deltas.shift();\n\n    if (deltas.length < 24) return;\n\n    const median = [...deltas].sort((a, b) => a - b)[12];\n    const next = median > 24 ? Math.min(scaleIndex + 1, SCALES.length - 1) : median < 10 ? Math.max(scaleIndex - 1, 0) : scaleIndex;\n\n    if (next === scaleIndex) return;\n\n    scaleIndex = next;\n    deltas.length = 0;\n    setAttribute(SCALE, String(SCALES[scaleIndex]));\n  };\n\n  const tick = (now: number) => {\n    frameId = 0;\n\n    if (!canDraw()) return;\n\n    const dt = lastTick ? Math.min((now - lastTick) / 1000, 0.1) : 1 / 60;\n\n    if (lastTick) adaptScale(now - lastTick);\n\n    lastTick = now;\n\n    const moving = sync(dt);\n\n    if (flowing) flowTime += dt;\n\n    const busy = moving || flowing || now < tailUntil || animationsRunning();\n\n    if (busy) {\n      render(false);\n      frameId = requestAnimationFrame(tick);\n\n      return;\n    }\n\n    lastTick = 0;\n    render(true);\n    setAttribute(STATE, 'idle');\n  };\n\n  function wake() {\n    if (!canDraw()) return;\n\n    tailUntil = performance.now() + TAIL_MS;\n\n    if (hidden || !onscreen) {\n      setAttribute(STATE, 'paused');\n\n      return;\n    }\n\n    if (frameId) return;\n\n    setAttribute(STATE, 'running');\n    frameId = requestAnimationFrame(tick);\n  }\n\n  const evaluateMode = () => {\n    const html = document.documentElement;\n\n    const off =\n      html.getAttribute('data-lg-reduce-transparency') === '1' || contrastQuery.matches || forcedQuery.matches;\n\n    reducedMotion = html.getAttribute('data-lg-reduce-motion') === '1' || motionQuery.matches;\n    lowQuality = html.getAttribute('data-lg-quality') === 'low';\n\n    if (off && mode === 'on') {\n      mode = 'off';\n      stop();\n      deactivate();\n      setAttribute(RENDERER, 'off');\n      setAttribute(STATE, 'idle');\n    } else if (!off && mode === 'off') {\n      mode = 'on';\n      setAttribute(RENDERER, 'css');\n      snapPresence = true;\n    }\n\n    dirty = true;\n    wake();\n  };\n\n  const compile = (type: number, source: string) => {\n    const shader = gl.createShader(type);\n\n    if (!shader) return null;\n\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n\n    if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) || gl.isContextLost()) return shader;\n\n    warnOnce('shader', `the glass shader failed to compile, so the scene keeps its CSS glass. ${gl.getShaderInfoLog(shader) ?? ''}`);\n    gl.deleteShader(shader);\n\n    return null;\n  };\n\n  const build = (): Gpu | null => {\n    const vertex = compile(gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragment = compile(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n    const program = gl.createProgram();\n    const vao = gl.createVertexArray();\n    const cells = gl.createBuffer();\n    const texture = gl.createTexture();\n\n    if (!vertex || !fragment || !program || !vao || !cells || !texture) return null;\n\n    gl.attachShader(program, vertex);\n    gl.attachShader(program, fragment);\n    gl.linkProgram(program);\n    gl.deleteShader(vertex);\n    gl.deleteShader(fragment);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      if (!gl.isContextLost()) {\n        warnOnce('shader', `the glass shader failed to link, so the scene keeps its CSS glass. ${gl.getProgramInfoLog(program) ?? ''}`);\n      }\n\n      return null;\n    }\n\n    // Corners come from attribute 0, since Firefox emulates a missing\n    // attribute 0 on every draw.\n    gl.bindVertexArray(vao);\n    gl.bindBuffer(gl.ARRAY_BUFFER, cells);\n    gl.enableVertexAttribArray(0);\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 12, 0);\n    gl.enableVertexAttribArray(1);\n    gl.vertexAttribIPointer(1, 1, gl.UNSIGNED_INT, 12, 8);\n    gl.bindVertexArray(null);\n\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_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    gl.disable(gl.BLEND);\n    gl.disable(gl.DEPTH_TEST);\n\n    // SAFETY: the entries come from UNIFORMS, so every Uniforms key is present.\n    const uniforms = Object.fromEntries(\n      UNIFORMS.map((name) => [name, gl.getUniformLocation(program, name)]),\n    ) as Uniforms;\n\n    // SAFETY: MAX_TEXTURE_SIZE is a GLint parameter, which WebGL returns as a number.\n    return { program, vao, cells, texture, uniforms, maxTexture: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number };\n  };\n\n  const start = () => {\n    gpu = build();\n\n    if (!gpu) {\n      if (!gl.isContextLost()) {\n        failed = true;\n        setAttribute(RENDERER, 'css');\n      }\n\n      return;\n    }\n\n    uploaded = false;\n    dirty = true;\n    wake();\n  };\n\n  const onContextLost = (event: Event) => {\n    event.preventDefault();\n    lost = true;\n    gpu = null;\n    uploaded = false;\n    stop();\n    deactivate();\n    setAttribute(RENDERER, 'css');\n    setAttribute(STATE, 'idle');\n    warnOnce('context-lost', 'the WebGL context was lost, so the scene shows its CSS glass until the browser restores it.');\n  };\n\n  const onContextRestored = () => {\n    lost = false;\n    start();\n  };\n\n  const onVisibility = () => {\n    hidden = document.hidden;\n\n    if (hidden) stop();\n\n    wake();\n  };\n\n  const watchResolution = () => {\n    resolutionQuery?.removeEventListener('change', onResolution);\n    resolutionQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);\n    resolutionQuery.addEventListener('change', onResolution);\n  };\n\n  function onResolution() {\n    watchResolution();\n    dirty = true;\n    wake();\n  }\n\n  const onChange = () => {\n    dirty = true;\n    wake();\n  };\n\n  // Mutations the renderer causes itself never wake it.\n  const rootObserver = new MutationObserver((mutations) => {\n    const relevant = mutations.some(\n      (mutation) =>\n        mutation.target !== canvas &&\n        mutation.target !== probe &&\n        mutation.target !== backdrop &&\n        !(mutation.type === 'attributes' && mutation.attributeName?.startsWith(ACTIVE)),\n    );\n\n    if (!relevant) return;\n\n    checkBackdrop();\n    claim();\n    wake();\n  });\n\n  const documentObserver = new MutationObserver(() => {\n    checkBackdrop();\n    evaluateMode();\n  });\n\n  const headObserver = new MutationObserver(() => {\n    checkBackdrop();\n    wake();\n  });\n\n  const resizeObserver = new ResizeObserver(onChange);\n\n  const intersectionObserver = new IntersectionObserver((entries) => {\n    onscreen = entries[entries.length - 1]?.isIntersecting ?? true;\n\n    if (!onscreen) stop();\n\n    wake();\n  });\n\n  const unsubscribe = registry.subscribe(() => {\n    claim();\n    onChange();\n  });\n\n  const wakeEvents = ['focusin', 'focusout', 'transitionrun', 'animationstart'] as const;\n\n  rootObserver.observe(root, { attributes: true, childList: true, subtree: true, characterData: true });\n  documentObserver.observe(document.documentElement, { attributes: true });\n  headObserver.observe(document.head, { childList: true, subtree: true, characterData: true });\n  resizeObserver.observe(root);\n  intersectionObserver.observe(root);\n  wakeEvents.forEach((type) => root.addEventListener(type, onChange));\n  window.addEventListener('resize', onChange);\n  window.addEventListener('scroll', onChange, { capture: true, passive: true });\n  document.addEventListener('visibilitychange', onVisibility);\n  document.fonts?.addEventListener('loadingdone', onChange);\n  [motionQuery, contrastQuery, forcedQuery].forEach((query) => query.addEventListener('change', evaluateMode));\n  canvas.addEventListener('webglcontextlost', onContextLost);\n  canvas.addEventListener('webglcontextrestored', onContextRestored);\n  watchResolution();\n\n  setAttribute(RENDERER, 'css');\n  setAttribute(SCALE, String(SCALES[scaleIndex]));\n  measureBox();\n  checkBackdrop();\n  evaluateMode();\n\n  if (!lost) start();\n\n  return {\n    update(next) {\n      settings = next;\n      dirty = true;\n      wake();\n    },\n    destroy() {\n      destroyed = true;\n      stop();\n      rootObserver.disconnect();\n      documentObserver.disconnect();\n      headObserver.disconnect();\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      unsubscribe();\n      wakeEvents.forEach((type) => root.removeEventListener(type, onChange));\n      window.removeEventListener('resize', onChange);\n      window.removeEventListener('scroll', onChange, { capture: true });\n      document.removeEventListener('visibilitychange', onVisibility);\n      document.fonts?.removeEventListener('loadingdone', onChange);\n      [motionQuery, contrastQuery, forcedQuery].forEach((query) => query.removeEventListener('change', evaluateMode));\n      resolutionQuery?.removeEventListener('change', onResolution);\n      canvas.removeEventListener('webglcontextlost', onContextLost);\n      canvas.removeEventListener('webglcontextrestored', onContextRestored);\n      loadGeneration += 1;\n      deactivate();\n      probe?.remove();\n\n      if (gpu && !gl.isContextLost()) {\n        gl.deleteProgram(gpu.program);\n        gl.deleteVertexArray(gpu.vao);\n        gl.deleteBuffer(gpu.cells);\n        gl.deleteTexture(gpu.texture);\n      }\n\n      gpu = null;\n      // A remount in StrictMode reuses this canvas and its context, so the\n      // context is released by shrinking it, never by losing it.\n      canvas.width = 1;\n      canvas.height = 1;\n      setAttribute(RENDERER, 'css');\n      [STATE, BACKDROP, SCALE].forEach((name) => root.removeAttribute(name));\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/scene-renderer.ts"
    },
    {
      "path": "registry/glassyui/lib/liquid-motion.ts",
      "content": "'use client';\n\nimport { useLayoutEffect } from 'react';\nimport type { RefObject } from 'react';\n\nexport type Spring = {\n  value: number;\n  velocity: number;\n  target: number;\n  stiffness: number;\n  damping: number;\n  precision: number;\n};\n\n/*\n * Springs take a response and a bounce: response is the period of the\n * undamped oscillation in seconds, and bounce 0 is critically damped. For\n * unit mass that is stiffness (2π / response)² and damping\n * 4π (1 - bounce) / response.\n */\nexport const createSpring = (\n  response: number,\n  bounce: number,\n  value: number,\n  precision = 0.001,\n): Spring => ({\n  value,\n  velocity: 0,\n  target: value,\n  stiffness: ((2 * Math.PI) / response) ** 2,\n  damping: (4 * Math.PI * (1 - bounce)) / response,\n  precision,\n});\n\n// Changes how a spring moves without stopping it, so it carries its velocity\n// into the new motion.\nexport const retuneSpring = (spring: Spring, response: number, bounce: number) => {\n  spring.stiffness = ((2 * Math.PI) / response) ** 2;\n  spring.damping = (4 * Math.PI * (1 - bounce)) / response;\n};\n\n// Semi-implicit Euler in steps of at most 1/120 s is stable for every spring\n// here. Returns whether the spring is still moving.\nexport const stepSpring = (spring: Spring, dt: number, snap: boolean) => {\n  if (snap || (spring.value === spring.target && spring.velocity === 0)) {\n    spring.value = spring.target;\n    spring.velocity = 0;\n\n    return false;\n  }\n\n  const steps = Math.max(1, Math.ceil(dt * 120));\n  const step = dt / steps;\n\n  for (let index = 0; index < steps; index += 1) {\n    spring.velocity +=\n      (-spring.stiffness * (spring.value - spring.target) - spring.damping * spring.velocity) *\n      step;\n    spring.value += spring.velocity * step;\n  }\n\n  if (\n    Math.abs(spring.value - spring.target) < spring.precision &&\n    Math.abs(spring.velocity) < spring.precision * 10\n  ) {\n    spring.value = spring.target;\n    spring.velocity = 0;\n\n    return false;\n  }\n\n  return true;\n};\n\nlet motionQuery: MediaQueryList | undefined;\n\n// The provider mirrors its preference onto <html>; the system setting always\n// counts, as it does in liquid.css.\nexport function motionReduced() {\n  motionQuery ??= window.matchMedia('(prefers-reduced-motion: reduce)');\n\n  return (\n    document.documentElement.getAttribute('data-lg-reduce-motion') === '1' || motionQuery.matches\n  );\n}\n\nexport type FrameLoop = {\n  start: () => void;\n  stop: () => void;\n};\n\n// Calls step once a frame with the seconds since the last one until it\n// returns false. A long pause counts as one short frame.\nexport function createFrameLoop(step: (dt: number) => boolean): FrameLoop {\n  let frame = 0;\n  let last = 0;\n\n  const tick = (now: number) => {\n    const dt = last ? Math.min((now - last) / 1000, 1 / 20) : 1 / 60;\n\n    last = now;\n    frame = 0;\n\n    if (step(dt)) {\n      frame = requestAnimationFrame(tick);\n\n      return;\n    }\n\n    last = 0;\n  };\n\n  return {\n    start() {\n      if (!frame) frame = requestAnimationFrame(tick);\n    },\n    stop() {\n      cancelAnimationFrame(frame);\n      frame = 0;\n      last = 0;\n    },\n  };\n}\n\ntype Edge = 'left' | 'top' | 'right' | 'bottom';\n\ntype Box = Record<Edge, number>;\n\ntype Feel = readonly [response: number, bounce: number];\n\nconst EDGES: readonly Edge[] = ['left', 'top', 'right', 'bottom'];\n\n// The edge heading for the new item leads and the far edge follows, so the\n// bubble stretches into a drop on the way and rounds off when it lands. The\n// leading edge overshoots a little and rings back, which is what makes the\n// landing feel like a drop settling rather than a slide stopping.\nconst LEAD: Feel = [0.2, 0.32];\n\nconst TRAIL: Feel = [0.3, 0.12];\n\nconst EVEN: Feel = [0.26, 0.22];\n\n// Under a finger the bubble keeps up closely but still leans into the drag.\nconst DRAG_LEAD: Feel = [0.1, 0.12];\n\nconst DRAG_TRAIL: Feel = [0.18, 0.04];\n\n// How far a lifted bubble grows past its item, in CSS pixels. It never grows\n// past the control's own edge. Kept to a hair: more reads as the bubble\n// zooming out of its track.\nconst SWELL = 1;\n\n// How much a lifted bubble magnifies the label under it.\nconst MAGNIFY = 0.04;\n\nconst DRAG_THRESHOLD = 4;\n\n// The bubble counts as travelling, and stays lifted, while an edge moves faster\n// than this in CSS pixels a second, so it settles the moment it slows down.\nconst TRAVEL_SPEED = 90;\n\n// How far a press on another item wakes the bubble before it moves.\nconst ANTICIPATION = 0.5;\n\ntype Frame = { element: HTMLElement; rect: DOMRect; scale: number };\n\n// The element the bubble is positioned in, with the scale of any scaled\n// ancestor, such as a dialog that is still opening. offsetWidth is rounded,\n// so a ratio within that rounding of 1 is no scaling at all.\nfunction measureFrame(element: HTMLElement): Frame {\n  const rect = element.getBoundingClientRect();\n  const ratio = rect.width / Math.max(element.offsetWidth, 1);\n\n  return { element, rect, scale: Math.abs(ratio - 1) * element.offsetWidth <= 0.5 ? 1 : ratio };\n}\n\n// Client rectangles keep the sub-pixel positions that layout offsets round\n// away, so the bubble lines up exactly. The magnification painted on an item\n// scales it about its centre and is divided back out. The bubble is placed in\n// the frame's scrolled content, so any scroll is added back.\nfunction boxWithin(element: HTMLElement, frame: Frame): Box {\n  const rect = element.getBoundingClientRect();\n  const own = Number(element.style.scale) || 1;\n  const width = rect.width / frame.scale / own;\n  const height = rect.height / frame.scale / own;\n  const { clientLeft, clientTop, scrollLeft, scrollTop } = frame.element;\n\n  const centreX =\n    (rect.left + rect.width / 2 - frame.rect.left) / frame.scale - clientLeft + scrollLeft;\n\n  const centreY =\n    (rect.top + rect.height / 2 - frame.rect.top) / frame.scale - clientTop + scrollTop;\n\n  return {\n    left: centreX - width / 2,\n    top: centreY - height / 2,\n    right: centreX + width / 2,\n    bottom: centreY + height / 2,\n  };\n}\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nexport type LiquidIndicatorOptions = {\n  /** Matches the item the bubble sits under. */\n  selected: string;\n  /** Matches every item the bubble can move to. */\n  item: string;\n  /** Lets a pointer pick the bubble up and drop it on another item. */\n  draggable?: boolean;\n  /** Strength of the bubble's stretch, lift and label magnification. */\n  deformation?: number;\n};\n\n/**\n * Moves a selection bubble between items like a drop of liquid glass: it\n * lifts, stretches toward the new item, lands with a little bounce and\n * magnifies the label under it while lifted. CSS places the bubble until this\n * runs. Reduced motion jumps instead.\n */\nexport function useLiquidIndicator(\n  rootRef: RefObject<HTMLElement | null>,\n  indicatorRef: RefObject<HTMLElement | null>,\n  { selected, item, draggable = false, deformation = 1 }: LiquidIndicatorOptions,\n) {\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    const indicator = indicatorRef.current;\n\n    if (!root || !indicator) return;\n\n    const edges: Record<Edge, Spring> = {\n      left: createSpring(...EVEN, 0, 0.05),\n      top: createSpring(...EVEN, 0, 0.05),\n      right: createSpring(...EVEN, 0, 0.05),\n      bottom: createSpring(...EVEN, 0, 0.05),\n    };\n\n    const lift = createSpring(0.22, 0.2, 0);\n    const painted = new Map<HTMLElement, string>();\n    let items: HTMLElement[] = [];\n    let centres: number[] = [];\n    let widths: number[] = [];\n    let room = 0;\n    let placed = false;\n    let held: Element | null = null;\n    let drag: { pointer: number; startX: number; offset: number; active: boolean } | null = null;\n\n    const frameOf = () =>\n      indicator.offsetParent instanceof HTMLElement ? indicator.offsetParent : root;\n\n    const paint = () => {\n      const width = edges.right.value - edges.left.value;\n      const height = edges.bottom.value - edges.top.value;\n      const restWidth = Math.max(edges.right.target - edges.left.target, 1);\n      const restHeight = Math.max(edges.bottom.target - edges.top.target, 1);\n      // A stretched drop keeps its volume, so it thins across its travel.\n      const thinY = clamp(1 / Math.sqrt(Math.max(width / restWidth, 1)), 0.8, 1);\n      const thinX = clamp(1 / Math.sqrt(Math.max(height / restHeight, 1)), 0.8, 1);\n      // A spring that rings past rest must not lift the bubble below its resting size.\n      const raised = Math.max(lift.value, 0) * deformation;\n      const swell = raised * Math.min(room, SWELL);\n      const drawnWidth = restWidth + (width * thinX - restWidth) * deformation + swell * 2;\n      const drawnHeight = restHeight + (height * thinY - restHeight) * deformation + swell * 2;\n      const centreX = (edges.left.value + edges.right.value) / 2;\n      const centreY = (edges.top.value + edges.bottom.value) / 2;\n\n      indicator.style.translate = `${centreX - drawnWidth / 2}px ${centreY - drawnHeight / 2}px`;\n      indicator.style.width = `${drawnWidth}px`;\n      indicator.style.height = `${drawnHeight}px`;\n      indicator.style.setProperty('--lg-lift', raised.toFixed(3));\n\n      items.forEach((element, index) => {\n        const nearness = Math.max(\n          0,\n          1 - Math.abs(centres[index] - centreX) / Math.max(widths[index], 1),\n        );\n\n        const magnify = 1 + raised * MAGNIFY * nearness;\n        const scale = magnify > 1.0005 ? magnify.toFixed(4) : '';\n\n        if ((painted.get(element) ?? '') === scale) return;\n\n        painted.set(element, scale);\n        element.style.scale = scale;\n      });\n    };\n\n    const step = (dt: number) => {\n      const reduced = motionReduced();\n      const travelling = EDGES.some((edge) => Math.abs(edges[edge].velocity) > TRAVEL_SPEED);\n      const pressed = held?.matches(selected) ?? false;\n      const woken = held !== null ? ANTICIPATION : 0;\n\n      lift.target = reduced ? 0 : pressed || drag?.active || travelling ? 1 : woken;\n      let moving = false;\n\n      for (const edge of EDGES) {\n        moving = stepSpring(edges[edge], dt, reduced) || moving;\n      }\n\n      moving = stepSpring(lift, dt, reduced) || moving;\n      paint();\n\n      return moving;\n    };\n\n    const loop = createFrameLoop(step);\n\n    const current = (): Box => ({\n      left: edges.left.value,\n      top: edges.top.value,\n      right: edges.right.value,\n      bottom: edges.bottom.value,\n    });\n\n    // Tunes each axis so the edge in front leads and the one behind trails.\n    const lean = (from: Box, to: Box, lead: Feel, trail: Feel) => {\n      const axes: [Edge, Edge, number][] = [\n        ['left', 'right', (to.left + to.right - from.left - from.right) / 2],\n        ['top', 'bottom', (to.top + to.bottom - from.top - from.bottom) / 2],\n      ];\n\n      for (const [start, end, distance] of axes) {\n        if (Math.abs(distance) < 1) {\n          retuneSpring(edges[start], ...EVEN);\n          retuneSpring(edges[end], ...EVEN);\n        } else {\n          retuneSpring(edges[distance > 0 ? end : start], ...lead);\n          retuneSpring(edges[distance > 0 ? start : end], ...trail);\n        }\n      }\n    };\n\n    const aim = (to: Box, jump: boolean) => {\n      for (const edge of EDGES) {\n        edges[edge].target = to[edge];\n\n        if (jump) {\n          edges[edge].value = to[edge];\n          edges[edge].velocity = 0;\n        }\n      }\n    };\n\n    const sync = (snap: boolean) => {\n      const frame = measureFrame(frameOf());\n      const target = root.querySelector<HTMLElement>(selected);\n\n      items = Array.from(root.querySelectorAll<HTMLElement>(item));\n\n      const boxes = items.map((element) => boxWithin(element, frame));\n\n      centres = boxes.map((box) => (box.left + box.right) / 2);\n      widths = boxes.map((box) => box.right - box.left);\n\n      painted.forEach((_, element) => {\n        if (items.includes(element)) return;\n\n        element.style.removeProperty('scale');\n        painted.delete(element);\n      });\n\n      // A pointer owns the bubble until it lets go.\n      if (drag?.active) return;\n\n      if (!target) {\n        indicator.setAttribute('data-lg-empty', '');\n        placed = false;\n\n        return;\n      }\n\n      const box = boxWithin(target, frame);\n      const jump = snap || !placed || motionReduced();\n\n      indicator.removeAttribute('data-lg-empty');\n      room = Math.max(0, Math.min(box.top, frame.element.clientHeight - box.bottom));\n\n      if (!jump) lean(current(), box, LEAD, TRAIL);\n\n      aim(box, jump);\n      placed = true;\n\n      if (jump) paint();\n\n      loop.start();\n    };\n\n    const toLocalX = (clientX: number) => {\n      const frame = measureFrame(frameOf());\n\n      return (\n        (clientX - frame.rect.left) / frame.scale -\n        frame.element.clientLeft +\n        frame.element.scrollLeft\n      );\n    };\n\n    const follow = (clientX: number) => {\n      if (!drag || centres.length === 0) return;\n\n      const width = edges.right.target - edges.left.target;\n\n      const centre = clamp(\n        toLocalX(clientX) - drag.offset,\n        Math.min(...centres),\n        Math.max(...centres),\n      );\n\n      const from = current();\n      const to = { ...from, left: centre - width / 2, right: centre + width / 2 };\n\n      lean(from, to, DRAG_LEAD, DRAG_TRAIL);\n      aim({ ...to, top: edges.top.target, bottom: edges.bottom.target }, motionReduced());\n      loop.start();\n    };\n\n    // Drops a dragged bubble on the nearest enabled item, or puts it back.\n    const release = (commit: boolean) => {\n      if (!drag) return;\n\n      const { active, pointer } = drag;\n\n      drag = null;\n      held = null;\n      root.removeAttribute('data-lg-dragging');\n\n      if (root.hasPointerCapture(pointer)) root.releasePointerCapture(pointer);\n\n      if (active && commit) {\n        const centre = (edges.left.target + edges.right.target) / 2;\n        let nearest = -1;\n\n        centres.forEach((value, index) => {\n          if (items[index].matches('[data-disabled]')) return;\n\n          if (nearest < 0 || Math.abs(value - centre) < Math.abs(centres[nearest] - centre))\n            nearest = index;\n        });\n\n        if (nearest >= 0) {\n          if (root.contains(document.activeElement)) items[nearest].focus({ preventScroll: true });\n\n          items[nearest].click();\n        }\n      }\n\n      sync(false);\n    };\n\n    const onPointerDown = (event: PointerEvent) => {\n      if (!event.isPrimary || event.button !== 0 || !(event.target instanceof Element)) return;\n\n      const pressed = event.target.closest(item);\n\n      if (\n        !(pressed instanceof HTMLElement) ||\n        !root.contains(pressed) ||\n        pressed.matches('[data-disabled]')\n      )\n        return;\n\n      held = pressed;\n      loop.start();\n\n      if (!draggable || !pressed.matches(selected)) return;\n\n      const centre = (edges.left.target + edges.right.target) / 2;\n\n      drag = {\n        pointer: event.pointerId,\n        startX: event.clientX,\n        offset: toLocalX(event.clientX) - centre,\n        active: false,\n      };\n    };\n\n    const onPointerMove = (event: PointerEvent) => {\n      if (!drag || event.pointerId !== drag.pointer) return;\n\n      if (!drag.active) {\n        if (Math.abs(event.clientX - drag.startX) < DRAG_THRESHOLD) return;\n\n        drag.active = true;\n        root.setAttribute('data-lg-dragging', '');\n        root.setPointerCapture(event.pointerId);\n      }\n\n      follow(event.clientX);\n    };\n\n    // Listens on the window, so a press that ends outside the control still ends.\n    const onPointerUp = (event: PointerEvent) => {\n      if (drag && event.pointerId === drag.pointer) {\n        release(event.type === 'pointerup');\n\n        return;\n      }\n\n      if (!held) return;\n\n      held = null;\n      loop.start();\n    };\n\n    // Taking the pointer from a touched item fires this on the item; only the\n    // root losing it ends a drag.\n    const onLostCapture = (event: PointerEvent) => {\n      if (event.target === root && drag?.active && event.pointerId === drag.pointer) release(false);\n    };\n\n    const resizes = new ResizeObserver(() => sync(true));\n\n    const observeSizes = () => {\n      resizes.disconnect();\n      resizes.observe(root);\n      root.querySelectorAll<HTMLElement>(item).forEach((element) => resizes.observe(element));\n    };\n\n    const mutations = new MutationObserver((records) => {\n      if (records.some((record) => record.type === 'childList')) observeSizes();\n\n      sync(false);\n    });\n\n    root.setAttribute('data-lg-indicator', 'ready');\n    sync(true);\n    observeSizes();\n    mutations.observe(root, {\n      attributes: true,\n      attributeFilter: ['data-state', 'data-disabled'],\n      childList: true,\n      subtree: true,\n    });\n    root.addEventListener('pointerdown', onPointerDown);\n    root.addEventListener('pointermove', onPointerMove);\n    root.addEventListener('lostpointercapture', onLostCapture);\n    window.addEventListener('pointerup', onPointerUp);\n    window.addEventListener('pointercancel', onPointerUp);\n\n    return () => {\n      loop.stop();\n      mutations.disconnect();\n      resizes.disconnect();\n      root.removeEventListener('pointerdown', onPointerDown);\n      root.removeEventListener('pointermove', onPointerMove);\n      root.removeEventListener('lostpointercapture', onLostCapture);\n      window.removeEventListener('pointerup', onPointerUp);\n      window.removeEventListener('pointercancel', onPointerUp);\n      root.removeAttribute('data-lg-indicator');\n      root.removeAttribute('data-lg-dragging');\n      painted.forEach((_, element) => element.style.removeProperty('scale'));\n      indicator.removeAttribute('data-lg-empty');\n      ['translate', 'width', 'height', '--lg-lift'].forEach((property) =>\n        indicator.style.removeProperty(property),\n      );\n    };\n  }, [deformation, draggable, indicatorRef, item, rootRef, selected]);\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/liquid-motion.ts"
    }
  ],
  "meta": {
    "sourceModules": [
      "scene"
    ]
  },
  "type": "registry:ui"
}