{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "AeroShards-TS-TW",
	"title": "AeroShards",
	"description": "A GPU-driven wind sculpture of folded foil shards with crisp detail, content-safe placements, and responsive pointer interactions.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "AeroShards/AeroShards.tsx",
			"content": "import { useEffect, useRef, useState } from 'react';\nimport { draw, effect, frame, init, sampler, surface, target, uniforms } from 'vgpu';\nimport type { Frame } from 'vgpu';\n\nconst PLACEMENTS = { right: 0, left: 1, center: 2, full: 3 };\nconst MATERIALS = { pearl: 0, chrome: 1, satin: 2 };\nconst INTERACTIONS = { none: 0, repel: 1, attract: 2 };\nconst EFFECTS = { none: 0, dither: 1, ascii: 2 };\nconst FLOWS = { stream: 0, vortex: 1, ribbon: 2 };\nconst RIPPLE_SPEED = 4.2;\nconst RIPPLE_TAIL = 1.8;\nconst MATERIAL_PRESETS = {\n  pearl: { roughness: 0.46, brightness: 0.92, glow: 0.54, highlightMix: 0.78 },\n  chrome: { roughness: 0.1, brightness: 1.12, glow: 0.38, highlightMix: 0.9 },\n  satin: { roughness: 0.74, brightness: 0.84, glow: 0.42, highlightMix: 0.66 }\n};\nconst DETAIL_PRESETS = {\n  bold: { count: 0.58, size: 1.32 },\n  balanced: { count: 1, size: 0.96 },\n  fine: { count: 1.15, size: 0.7 }\n};\nconst QUALITY_PRESETS = {\n  low: { count: 1900, dpr: 1.5, supersamplePixels: 3000000 },\n  medium: { count: 3200, dpr: 2, supersamplePixels: 6000000 },\n  high: { count: 4600, dpr: 2, supersamplePixels: 8000000 }\n};\nconst RUNTIME_QUALITY = [{ countScale: 1 }, { countScale: 0.86 }, { countScale: 0.72 }];\n// Only the halo is downsampled. Shard edges keep their display-resolution detail.\nconst BLOOM_SCALES = [0.25, 0.22, 0.18];\nconst FRAME_STATES = {\n  interactive: { interval: 1000 / 60, continuous: true },\n  settling: { interval: 1000 / 60, continuous: true },\n  ambient: { interval: 1000 / 60, continuous: true },\n  partial: { interval: 1000 / 12, continuous: false }\n};\n\nconst resolveFrameInterval = (frameState: FrameState, refreshInterval: number): number =>\n  frameState.continuous ? Math.max(frameState.interval, refreshInterval) : frameState.interval;\n\nconst advanceFrameDeadline = (timestamp: number, deadline: number, interval: number, reset: boolean): number => {\n  const nextDeadline = deadline + interval;\n  return reset || nextDeadline <= timestamp - 0.5 ? timestamp + interval : nextDeadline;\n};\n\ntype Color = [number, number, number, number];\ntype Placement = keyof typeof PLACEMENTS;\ntype Material = keyof typeof MATERIALS;\ntype Detail = keyof typeof DETAIL_PRESETS;\ntype Interaction = keyof typeof INTERACTIONS;\ntype Quality = keyof typeof QUALITY_PRESETS;\ntype QualityPreset = (typeof QUALITY_PRESETS)[Quality];\ntype FrameState = (typeof FRAME_STATES)[keyof typeof FRAME_STATES];\n\nexport interface AeroShardsProps {\n  backgroundColor?: string;\n  shardColor?: string;\n  accentColor?: string;\n  placement?: Placement;\n  flow?: keyof typeof FLOWS;\n  rippleIntensity?: number;\n  holdToGather?: boolean;\n  material?: Material;\n  detail?: Detail;\n  effect?: keyof typeof EFFECTS;\n  scale?: number;\n  spread?: number;\n  depth?: number;\n  speed?: number;\n  spin?: number;\n  interaction?: Interaction;\n  density?: number;\n  shardSize?: number;\n  stretch?: number;\n  turbulence?: number;\n  glow?: number;\n  edgeSoftness?: number;\n  bloom?: number;\n  grain?: number;\n  chromaticAberration?: number;\n  transitionDuration?: number;\n  interactionRadius?: number;\n  interactionStrength?: number;\n  paused?: boolean;\n  className?: string;\n  onError?: (error: Error) => void;\n}\n\ninterface AeroSettings {\n  background: Color;\n  shard: Color;\n  highlight: Color;\n  accent: Color;\n  composition: number;\n  flow: number;\n  rippleIntensity: number;\n  holdToGather: boolean;\n  effect: number;\n  material: number;\n  detailCount: number;\n  shardSize: number;\n  scale: number;\n  stretch: number;\n  speed: number;\n  spin: number;\n  turbulence: number;\n  spread: number;\n  depth: number;\n  roughness: number;\n  brightness: number;\n  glow: number;\n  edgeSoftness: number;\n  bloom: number;\n  grain: number;\n  chromaticAberration: number;\n  exposure: number;\n  lightSurface: number;\n  transitionDuration: number;\n  interaction: number;\n  interactionRadius: number;\n  interactionStrength: number;\n  paused: boolean;\n  signature: string;\n}\n\ninterface PointerState {\n  raw: [number, number];\n  position: [number, number];\n  velocity: [number, number];\n  active: number;\n  presence: number;\n  presenceVelocity: number;\n  initialized: boolean;\n}\n\ninterface HoldState {\n  pointerId: number | null;\n  elapsed: number;\n  amount: number;\n  velocity: number;\n  phase: number;\n}\n\ninterface RippleState {\n  origin: [number, number];\n  age: number;\n  duration: number;\n  strength: number;\n}\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));\n\nconst createFormation = (flow: number) => ({ weights: layoutVector(flow), velocity: [0, 0, 0, 0] });\n\nconst advanceFormation = (\n  state: ReturnType<typeof createFormation>,\n  flow: number,\n  elapsed: number,\n  duration: number,\n  frozen: boolean\n) => {\n  const goal = layoutVector(flow);\n  if (frozen) {\n    state.weights = goal;\n    state.velocity.fill(0);\n    return;\n  }\n  const response = 6 / duration;\n  const decay = Math.exp(-response * elapsed);\n  for (let i = 0; i < 4; i += 1) {\n    const offset = state.weights[i] - goal[i];\n    const momentum = state.velocity[i] + response * offset;\n    state.weights[i] = goal[i] + (offset + momentum * elapsed) * decay;\n    state.velocity[i] = (state.velocity[i] - response * momentum * elapsed) * decay;\n  }\n  if (state.weights.every((value, i) => Math.abs(value - goal[i]) < 0.0001 && Math.abs(state.velocity[i]) < 0.001)) {\n    state.weights = goal;\n    state.velocity.fill(0);\n  }\n};\n\nconst resolvePathLength = (aspect: number, weights: number[]) => {\n  const side = 2.65 + 0.61 * aspect + 0.09 * aspect * aspect;\n  const center = 2.3 + 2 * aspect + 0.35 * aspect * aspect;\n  const full = Math.hypot(2.44 * aspect, Math.sqrt(5));\n  const mobile = Math.hypot(2.56 * aspect, 1);\n  return aspect < 0.82\n    ? mobile * (weights[0] + weights[1] + weights[2]) + full * weights[3]\n    : side * (weights[0] + weights[1]) + center * weights[2] + full * weights[3];\n};\n\nconst createHold = (): HoldState => ({ pointerId: null, elapsed: 0, amount: 0, velocity: 0, phase: 0 });\n\nconst advanceHold = (hold: HoldState, elapsed: number, disabled: boolean) => {\n  if (disabled) {\n    hold.pointerId = null;\n    hold.elapsed = 0;\n    hold.amount = 0;\n    hold.velocity = 0;\n    return;\n  }\n  const previousElapsed = hold.elapsed;\n  hold.elapsed = hold.pointerId === null ? 0 : hold.elapsed + elapsed;\n  // A short click remains a ripple. Gathering starts only after a deliberate hold.\n  const engaging = hold.pointerId !== null && hold.elapsed > 0.15;\n  const step = engaging && previousElapsed < 0.15 ? hold.elapsed - 0.15 : elapsed;\n  const target = engaging ? 1 : 0;\n  const response = engaging ? 3.8 : 3.2;\n  const decay = Math.exp(-response * step);\n  const offset = hold.amount - target;\n  const momentum = hold.velocity + response * offset;\n  hold.amount = target + (offset + momentum * step) * decay;\n  hold.velocity = (hold.velocity - response * momentum * step) * decay;\n  if (Math.abs(hold.amount - target) < 0.0001 && Math.abs(hold.velocity) < 0.001) {\n    hold.amount = target;\n    hold.velocity = 0;\n  }\n  if (hold.amount > 0) hold.phase += elapsed * (0.35 + hold.amount * 0.5);\n};\n\nconst resetPointerMotion = (pointer: PointerState): void => {\n  pointer.velocity ??= [0, 0];\n  pointer.velocity[0] = 0;\n  pointer.velocity[1] = 0;\n  pointer.presenceVelocity = 0;\n};\n\nconst advancePointer = (pointer: PointerState, elapsed: number): void => {\n  if (elapsed <= 0) return;\n\n  // Exact critically damped motion keeps velocity continuous through direction changes.\n  const response = 26;\n  const decay = Math.exp(-response * elapsed);\n  for (let axis = 0; axis < 2; axis += 1) {\n    const offset = pointer.position[axis] - pointer.raw[axis];\n    const momentum = pointer.velocity[axis] + response * offset;\n    pointer.position[axis] = pointer.raw[axis] + (offset + momentum * elapsed) * decay;\n    pointer.velocity[axis] = (pointer.velocity[axis] - response * momentum * elapsed) * decay;\n  }\n\n  // Let the field ease into contact, then release a little more slowly.\n  const presenceTarget = pointer.active ? 1 : 0;\n  const presenceResponse = pointer.active ? 24 : 12;\n  const presenceDecay = Math.exp(-presenceResponse * elapsed);\n  const presenceOffset = pointer.presence - presenceTarget;\n  const presenceMomentum = pointer.presenceVelocity + presenceResponse * presenceOffset;\n  const nextPresence = presenceTarget + (presenceOffset + presenceMomentum * elapsed) * presenceDecay;\n  pointer.presence = clamp(nextPresence, 0, 1);\n  pointer.presenceVelocity = (pointer.presenceVelocity - presenceResponse * presenceMomentum * elapsed) * presenceDecay;\n\n  if (nextPresence !== pointer.presence) pointer.presenceVelocity = 0;\n  if (Math.abs(pointer.presence - presenceTarget) < 0.001 && Math.abs(pointer.presenceVelocity) < 0.01) {\n    pointer.presence = presenceTarget;\n    pointer.presenceVelocity = 0;\n  }\n};\n\nconst createRipples = (): RippleState[] =>\n  Array.from({ length: 4 }, () => ({ origin: [0.5, 0.5], age: 0, duration: 0, strength: 0 }));\n\nconst startRipple = (ripples: RippleState[], origin: [number, number], aspect: number, strength = 1): boolean => {\n  // Preserve waves already in flight; rapid clicks never reset a visible wave.\n  const ripple = ripples.find(value => value.strength === 0);\n  if (!ripple) return false;\n  ripple.origin = [...origin];\n  ripple.age = 0;\n  const farthestX = (1 + Math.abs(origin[0] * 2 - 1)) * aspect;\n  const farthestY = 1 + Math.abs(origin[1] * 2 - 1);\n  ripple.duration = Math.hypot(farthestX, farthestY) / RIPPLE_SPEED + RIPPLE_TAIL;\n  ripple.strength = strength;\n  return true;\n};\n\nconst advanceRipples = (ripples: RippleState[], elapsed: number, disabled: boolean): void => {\n  for (const ripple of ripples) {\n    if (disabled) ripple.strength = 0;\n    if (!ripple.strength) continue;\n    ripple.age += elapsed;\n    if (ripple.age >= ripple.duration) ripple.strength = 0;\n  }\n};\n\nconst layoutVector = (placement: number): number[] => [0, 1, 2, 3].map(index => (index === placement ? 1 : 0));\n\nconst mixColor = (from: Color, to: Color, amount: number): Color => [\n  from[0] + (to[0] - from[0]) * amount,\n  from[1] + (to[1] - from[1]) * amount,\n  from[2] + (to[2] - from[2]) * amount,\n  1\n];\n\nconst SHARD_SHADER = `\nstruct ViewParams {\n  viewport: vec4f,\n  shape: vec4f,\n  effects: vec4f,\n  composition: vec4f,\n  transport: vec4f,\n  formation: vec4f,\n  gather: vec4f,\n  pointer: vec4f,\n  shock: vec4f,\n  shockB: vec4f,\n  shockC: vec4f,\n  shockD: vec4f,\n  material: vec4f,\n  light: vec4f,\n  environment: vec4f,\n  baseColor: vec4f,\n  highlightColor: vec4f,\n  accentColor: vec4f,\n}\n\nstruct PathSample {\n  position: vec3f,\n  tangent: vec3f,\n  phase: f32,\n}\n\nstruct VertexOut {\n  @builtin(position) position: vec4f,\n  @location(0) @interpolate(flat, first) baseAlpha: vec4f,\n  @location(1) @interpolate(flat, first) creaseColor: vec3f,\n  @location(2) localCoord: vec2f,\n}\n\n@group(0) @binding(0) var<uniform> view: ViewParams;\n\nfn hashU32(value: u32) -> u32 {\n  var state = value * 747796405u + 2891336453u;\n  let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n  return (word >> 22u) ^ word;\n}\n\nfn unitFloat(value: u32) -> f32 {\n  return f32(hashU32(value)) * (1.0 / 4294967296.0);\n}\n\nfn safeNormalize(value: vec3f) -> vec3f {\n  return value / max(length(value), 0.0001);\n}\n\nfn safeNormalize2(value: vec2f) -> vec2f {\n  return value / max(length(value), 0.0001);\n}\n\nfn cubic(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {\n  let oneMinusT = 1.0 - t;\n  return oneMinusT * oneMinusT * oneMinusT * p0\n    + 3.0 * oneMinusT * oneMinusT * t * p1\n    + 3.0 * oneMinusT * t * t * p2\n    + t * t * t * p3;\n}\n\nfn cubicDerivative(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {\n  let oneMinusT = 1.0 - t;\n  return 3.0 * oneMinusT * oneMinusT * (p1 - p0)\n    + 6.0 * oneMinusT * t * (p2 - p1)\n    + 3.0 * t * t * (p3 - p2);\n}\n\nfn sideArc(phase: f32) -> f32 {\n  let lookup = array<f32, 32>(\n    0.000000, 0.052475, 0.097829, 0.135121, 0.166845, 0.195164, 0.221458, 0.246639,\n    0.271368, 0.296184, 0.321577, 0.348019, 0.375973, 0.405832, 0.437746, 0.471327,\n    0.505474, 0.538781, 0.570323, 0.599945, 0.628000, 0.655048, 0.681734, 0.708795,\n    0.737169, 0.768244, 0.804295, 0.848010, 0.894805, 0.935083, 0.969270, 1.000000,\n  );\n  let scaled = clamp(phase, 0.0, 0.999999) * 31.0;\n  let index = min(u32(floor(scaled)), 30u);\n  return mix(lookup[index], lookup[index + 1u], fract(scaled));\n}\n\nfn fullArc(phase: f32) -> f32 {\n  let lookup = array<f32, 32>(\n    0.000000, 0.028092, 0.055939, 0.083892, 0.112291, 0.141449, 0.171637, 0.203033,\n    0.235650, 0.269282, 0.303537, 0.337982, 0.372308, 0.406392, 0.440263, 0.474026,\n    0.507794, 0.541636, 0.575553, 0.609470, 0.643257, 0.676761, 0.709855, 0.742465,\n    0.774594, 0.806319, 0.837790, 0.869218, 0.900862, 0.933020, 0.965991, 1.000000,\n  );\n  let scaled = clamp(phase, 0.0, 0.999999) * 31.0;\n  let index = min(u32(floor(scaled)), 30u);\n  return mix(lookup[index], lookup[index + 1u], fract(scaled));\n}\n\nfn centerArc(phase: f32) -> f32 {\n  let lookup = array<f32, 32>(\n    0.000000, 0.028692, 0.059794, 0.096620, 0.140315, 0.179839, 0.212476, 0.241834,\n    0.270282, 0.299332, 0.329968, 0.362347, 0.395341, 0.427241, 0.457283, 0.485900,\n    0.514192, 0.543773, 0.577230, 0.618093, 0.661144, 0.696770, 0.727388, 0.756064,\n    0.784657, 0.814415, 0.845979, 0.878880, 0.911508, 0.942489, 0.971712, 1.000000,\n  );\n  let scaled = clamp(phase, 0.0, 0.999999) * 31.0;\n  let index = min(u32(floor(scaled)), 30u);\n  return mix(lookup[index], lookup[index + 1u], fract(scaled));\n}\n\nfn mobileArc(phase: f32) -> f32 {\n  let lookup = array<f32, 32>(\n    0.000000, 0.028885, 0.057970, 0.087431, 0.117400, 0.147935, 0.179017, 0.210560,\n    0.242467, 0.274689, 0.307272, 0.340367, 0.374193, 0.408970, 0.444794, 0.481483,\n    0.518517, 0.555206, 0.591030, 0.625807, 0.659633, 0.692728, 0.725311, 0.757533,\n    0.789440, 0.820983, 0.852065, 0.882600, 0.912569, 0.942030, 0.971115, 1.000000,\n  );\n  let scaled = clamp(phase, 0.0, 0.999999) * 31.0;\n  let index = min(u32(floor(scaled)), 30u);\n  return mix(lookup[index], lookup[index + 1u], fract(scaled));\n}\n\nfn sidePath(seedPhase: f32, distance: f32, aspect: f32, mirror: f32) -> PathSample {\n  let pi = 3.14159265359;\n  let pathLength = 2.65 + 0.61 * aspect + 0.09 * aspect * aspect;\n  let phase = fract(seedPhase + distance / pathLength);\n  let t = sideArc(phase);\n  let x = cubic(1.24, 1.02, -0.28, 0.12, t) + sin(t * pi * 4.0 + 0.34) * 0.055;\n  let y = cubic(1.38, 0.72, -0.56, -1.38, t) + sin(t * pi * 2.0 - 0.6) * 0.04;\n  let z = sin(t * pi * 3.0) * 0.18;\n  let derivative = vec3f(\n    mirror * aspect * (\n      cubicDerivative(1.24, 1.02, -0.28, 0.12, t)\n        + cos(t * pi * 4.0 + 0.34) * pi * 4.0 * 0.055\n    ),\n    cubicDerivative(1.38, 0.72, -0.56, -1.38, t)\n      + cos(t * pi * 2.0 - 0.6) * pi * 2.0 * 0.04,\n    cos(t * pi * 3.0) * pi * 3.0 * 0.18,\n  );\n  var sample: PathSample;\n  sample.position = vec3f(mirror * aspect * x, y, z);\n  sample.tangent = safeNormalize(derivative);\n  sample.phase = phase;\n  return sample;\n}\n\nfn centerPath(seedPhase: f32, distance: f32, aspect: f32) -> PathSample {\n  let pi = 3.14159265359;\n  let pathLength = 2.3 + 2.0 * aspect + 0.35 * aspect * aspect;\n  let phase = fract(seedPhase + distance / pathLength);\n  let t = centerArc(phase);\n  let angle = mix(-0.25 * pi, 1.75 * pi, t);\n  let angleDerivative = 2.0 * pi;\n  let radius = 0.72 + sin(t * pi * 4.0) * 0.12;\n  let radiusDerivative = cos(t * pi * 4.0) * pi * 4.0 * 0.12;\n  let derivative = vec3f(\n    aspect * (\n      -sin(angle) * angleDerivative * radius\n        + cos(angle) * radiusDerivative\n    ),\n    cos(angle) * angleDerivative * radius\n      + sin(angle) * radiusDerivative,\n    cos(t * pi * 2.0) * pi * 2.0 * 0.16,\n  );\n  var sample: PathSample;\n  sample.position = vec3f(\n    cos(angle) * radius * aspect,\n    sin(angle) * radius,\n    sin(t * pi * 2.0) * 0.16,\n  );\n  sample.tangent = safeNormalize(derivative);\n  sample.phase = phase;\n  return sample;\n}\n\nfn fullPath(seedPhase: f32, distance: f32, aspect: f32) -> PathSample {\n  let pi = 3.14159265359;\n  let pathWidth = 2.44 * aspect;\n  let pathLength = sqrt(pathWidth * pathWidth + 5.0);\n  let phase = fract(seedPhase + distance / pathLength);\n  let t = fullArc(phase);\n  let derivative = vec3f(\n    aspect * 2.44,\n    cos((t * 1.72 - 0.2) * pi) * 1.72 * pi * 0.54\n      + cos(t * pi * 3.0) * pi * 3.0 * 0.12,\n    -sin(t * pi * 2.0 - 0.7) * pi * 2.0 * 0.22,\n  );\n  var sample: PathSample;\n  sample.position = vec3f(\n    mix(-aspect * 1.22, aspect * 1.22, t),\n    sin((t * 1.72 - 0.2) * pi) * 0.54 + sin(t * pi * 3.0) * 0.12,\n    cos(t * pi * 2.0 - 0.7) * 0.22,\n  );\n  sample.tangent = safeNormalize(derivative);\n  sample.phase = phase;\n  return sample;\n}\n\nfn mobilePath(seedPhase: f32, distance: f32, aspect: f32) -> PathSample {\n  let pi = 3.14159265359;\n  let pathWidth = 2.56 * aspect;\n  let pathLength = sqrt(pathWidth * pathWidth + 1.0);\n  let phase = fract(seedPhase + distance / pathLength);\n  let t = mobileArc(phase);\n  let derivative = vec3f(\n    aspect * 2.56,\n    cos(t * pi) * pi * 0.28 + cos(t * pi * 3.0) * pi * 3.0 * 0.06,\n    -sin(t * pi * 2.0) * pi * 2.0 * 0.16,\n  );\n  var sample: PathSample;\n  sample.position = vec3f(\n    mix(-aspect * 1.28, aspect * 1.28, t),\n    -0.86 + sin(t * pi) * 0.28 + sin(t * pi * 3.0) * 0.06,\n    cos(t * pi * 2.0) * 0.16,\n  );\n  sample.tangent = safeNormalize(derivative);\n  sample.phase = phase;\n  return sample;\n}\n\nfn weightedPath(seedPhase: f32, phaseOffset: f32, aspect: f32, weights: vec4f) -> PathSample {\n  // Every placement samples the same point along the stream, including its wrap seam.\n  let phase = fract(seedPhase + phaseOffset);\n  var result: PathSample;\n  result.position = vec3f(0.0);\n  result.tangent = vec3f(0.0);\n  result.phase = phase;\n\n  if (aspect < 0.82) {\n    let compactWeight = weights.x + weights.y + weights.z;\n    if (compactWeight > 0.0001) {\n      let compact = mobilePath(phase, 0.0, aspect);\n      result.position += compact.position * compactWeight;\n      result.tangent += compact.tangent * compactWeight;\n    }\n    if (weights.w > 0.0001) {\n      let wide = fullPath(phase, 0.0, aspect);\n      result.position += wide.position * weights.w;\n      result.tangent += wide.tangent * weights.w;\n    }\n  } else {\n    if (weights.x > 0.0001) {\n      let right = sidePath(phase, 0.0, aspect, 1.0);\n      result.position += right.position * weights.x;\n      result.tangent += right.tangent * weights.x;\n    }\n    if (weights.y > 0.0001) {\n      let left = sidePath(phase, 0.0, aspect, -1.0);\n      result.position += left.position * weights.y;\n      result.tangent += left.tangent * weights.y;\n    }\n    if (weights.z > 0.0001) {\n      let center = centerPath(phase, 0.0, aspect);\n      result.position += center.position * weights.z;\n      result.tangent += center.tangent * weights.z;\n    }\n    if (weights.w > 0.0001) {\n      let wide = fullPath(phase, 0.0, aspect);\n      result.position += wide.position * weights.w;\n      result.tangent += wide.tangent * weights.w;\n    }\n  }\n\n  result.tangent = safeNormalize(result.tangent + vec3f(0.0001, 0.0, 0.0));\n  return result;\n}\n\nfn pointerField(delta: vec2f, radius: f32, flow: vec2f, depth: f32) -> vec2f {\n  // A curved Gaussian follows the flow, with a long, boundary-free tail.\n  let offset = delta / max(radius, 0.001);\n  let along = dot(offset, flow);\n  let across = dot(offset, vec2f(-flow.y, flow.x));\n  let alongSquared = along * along;\n  let layer = depth * inverseSqrt(1.0 + depth * depth);\n  let bend = (0.22 * alongSquared + 0.12 * layer * along) / (1.0 + alongSquared);\n  let curvedAcross = (across + bend) / (1.0 + layer * 0.18);\n  let falloff = exp(-0.28 * alongSquared - 1.2 * curvedAcross * curvedAcross);\n  return offset * falloff;\n}\n\nfn rippleWave(age: f32) -> f32 {\n  if (age <= 0.0 || age >= ${RIPPLE_TAIL}) { return 0.0; }\n  let attack = smoothstep(0.0, 0.14, age);\n  let release = 1.0 - smoothstep(1.4, ${RIPPLE_TAIL}, age);\n  return sin(age * 10.0) * exp(-age * 3.2) * attack * release;\n}\n\nfn rippleDisplacement(position: vec3f, pulse: vec4f) -> vec4f {\n  if (pulse.w <= 0.0001) { return vec4f(0.0); }\n  let perspective = 1.0 / max(0.62, 1.0 - position.z * 0.34);\n  let delta = (position.xy * perspective - pulse.xy) * view.viewport.z;\n  let distance = sqrt(dot(delta, delta) + 0.0016) - 0.04;\n  let wave = rippleWave(pulse.z - distance / ${RIPPLE_SPEED}) * pulse.w;\n  let radial = delta / (distance + 0.12);\n  return vec4f(radial * wave * 0.28, wave * 0.12, abs(wave));\n}\n\nfn shardVertex(index: u32) -> vec3f {\n  let fold = 0.34;\n  let vertices = array<vec3f, 6>(\n    vec3f(0.0, 1.0, fold),\n    vec3f(-0.72, 0.0, 0.0),\n    vec3f(0.0, -1.0, fold),\n    vec3f(0.0, 1.0, fold),\n    vec3f(0.0, -1.0, fold),\n    vec3f(0.72, 0.0, 0.0)\n  );\n  return vertices[index % 6u];\n}\n\nfn softbox(direction: vec3f, center: vec2f, size: vec2f) -> f32 {\n  let q = abs((direction.xy - center) / size);\n  let q2 = q * q;\n  let q4 = q2 * q2;\n  return exp(-(q4.x + q4.y));\n}\n\nfn aces(color: vec3f) -> vec3f {\n  let a = 2.51;\n  let b = 0.03;\n  let c = 2.43;\n  let d = 0.59;\n  let e = 0.14;\n  return clamp((color * (a * color + b)) / (color * (c * color + d) + e), vec3f(0.0), vec3f(1.0));\n}\n\n@vertex\nfn vs_main(\n  @builtin(vertex_index) vertexIndex: u32,\n  @builtin(instance_index) instanceIndex: u32,\n) -> VertexOut {\n  let seedPhase = unitFloat(instanceIndex * 1664525u + 1013904223u);\n  let seedLane = unitFloat(instanceIndex * 2246822519u + 3266489917u);\n  let seedDepth = unitFloat(instanceIndex * 668265263u + 374761393u);\n  let seedScale = unitFloat(instanceIndex * 1597334677u + 3812015801u);\n  let aspect = view.viewport.x;\n  let path = weightedPath(seedPhase, view.transport.x, aspect, view.composition);\n  var direction = path.tangent;\n  var planarNormal = safeNormalize2(vec2f(-direction.y, direction.x));\n\n  let signedLane = seedLane * 2.0 - 1.0;\n  let lane = sign(signedLane) * pow(abs(signedLane), 0.72);\n  let widthProfile = 0.46 + pow(max(sin(path.phase * 3.14159265359), 0.0), 0.72) * 0.54;\n  let looseSeed = unitFloat(instanceIndex * 3266489917u + 668265263u);\n  let loose = smoothstep(0.92, 1.0, looseSeed);\n  let flowWave = sin(path.phase * 37.6991118431 + seedDepth * 12.0);\n  let laneWidth = (lane * 0.56 + flowWave * 0.055 * view.shape.z) * view.shape.x\n    * widthProfile * (1.0 + loose * 0.72);\n  let depthLane = (seedDepth * 2.0 - 1.0) * view.shape.y\n    + cos(path.phase * 31.4159265359 + seedLane * 8.0) * 0.06 * view.shape.z;\n  var renderPosition = path.position + vec3f(planarNormal * laneWidth, depthLane);\n\n  if (view.formation.y + view.formation.z > 0.00001) {\n    let center = vec2f((view.composition.x - view.composition.y) * aspect * 0.56, 0.0);\n    var formedPosition = renderPosition * view.formation.x;\n    var formedDirection = direction * view.formation.x;\n    if (view.formation.y > 0.00001) {\n      let radius = 0.16 + sqrt(seedLane) * 0.74 * (0.45 + view.shape.x * 0.55);\n      // Constant tangential travel speed; inner rings turn faster without speeding up.\n      let angle = seedPhase * 6.28318530718 + view.viewport.w / radius;\n      let radial = vec2f(cos(angle), sin(angle));\n      let position = vec3f(center + radial * radius, (seedDepth - 0.5) * view.shape.y * 0.65 + radial.y * 0.2);\n      formedPosition += position * view.formation.y;\n      formedDirection += safeNormalize(vec3f(-radial.y, radial.x, radial.x * 0.2)) * view.formation.y;\n    }\n    if (view.formation.z > 0.00001) {\n      let phase = fract(seedPhase + view.viewport.w / (aspect * 3.0 + 2.0));\n      let angle = phase * 6.28318530718;\n      let ribbonWidth = (seedLane - 0.5) * 0.54 * view.shape.x;\n      let position = vec3f(\n        mix(-aspect * 1.35, aspect * 1.35, phase) + center.x * 0.5,\n        sin(angle) * 0.42 + cos(angle * 2.0) * ribbonWidth,\n        (cos(angle) * 0.35 + sin(angle * 2.0) * ribbonWidth + (seedDepth - 0.5) * 0.12) * view.shape.y\n      );\n      let tangent = safeNormalize(vec3f(aspect * 2.7, cos(angle) * 2.638938, -sin(angle) * 2.199115 * view.shape.y));\n      formedPosition += position * view.formation.z;\n      formedDirection += tangent * view.formation.z;\n    }\n    renderPosition = formedPosition;\n    direction = safeNormalize(formedDirection + vec3f(0.0, 0.0, 0.02 * view.formation.x * (1.0 - view.formation.x)));\n    planarNormal = safeNormalize2(vec2f(-direction.y, direction.x));\n  }\n\n  if (abs(view.pointer.w) > 0.0001) {\n    let field = pointerField(\n      view.pointer.xy - renderPosition.xy,\n      view.pointer.z,\n      vec2f(planarNormal.y, -planarNormal.x),\n      renderPosition.z,\n    );\n    let lateral = field - direction.xy * dot(field, direction.xy);\n    renderPosition += vec3f(lateral * view.pointer.w * 0.36, 0.0);\n    direction = safeNormalize(vec3f(direction.xy + lateral * view.pointer.w * 0.65, direction.z));\n  }\n\n  if (view.gather.z > 0.00001) {\n    // A Gaussian cloud has a dense center and soft outskirts, never a ring or a hard outline.\n    let relative = (renderPosition.xy - view.gather.xy) * view.viewport.z;\n    let reach = length(relative);\n    let radius = sqrt(-2.0 * log(max(seedLane, 0.0001)));\n    let angle = seedPhase * 6.28318530718 + view.gather.w * (0.3 + seedDepth * 0.18);\n    let orbit = vec2f(cos(angle), sin(angle));\n    let layer = seedDepth * 6.28318530718;\n    let drift = vec2f(sin(layer + view.gather.w * 0.22), cos(layer * 1.7 - view.gather.w * 0.18)) * 0.055;\n    let cloud = orbit * radius * vec2f(0.2, 0.16) + drift;\n    let cluster = vec3f(view.gather.xy + cloud / view.viewport.z, (seedDepth - 0.5) * 0.42);\n    // Distant layers arrive later; there is no single closing boundary.\n    let amount = pow(view.gather.z, 1.0 + seedDepth * 0.65 + min(reach, 4.0) * 0.12);\n    let curledDirection = safeNormalize(vec3f(-orbit.y, orbit.x, sin(layer) * 0.35));\n    renderPosition = mix(renderPosition, cluster, amount);\n    direction = safeNormalize(mix(direction, curledDirection, amount));\n  }\n\n  var rippleLight = 0.0;\n  if (view.shock.w + view.shockB.w + view.shockC.w + view.shockD.w > 0.0001) {\n    let displacement = rippleDisplacement(renderPosition, view.shock)\n      + rippleDisplacement(renderPosition, view.shockB)\n      + rippleDisplacement(renderPosition, view.shockC)\n      + rippleDisplacement(renderPosition, view.shockD);\n    rippleLight = min(displacement.w, 1.5);\n    if (dot(displacement.xyz, displacement.xyz) > 0.0) {\n      renderPosition += displacement.xyz;\n      direction = safeNormalize(direction + displacement.xyz * 0.7);\n    }\n  }\n\n  let shapeLocal = shardVertex(vertexIndex);\n  var local = shapeLocal;\n  local.x *= mix(0.72, 1.08, seedLane);\n  local.y *= mix(0.82, 1.12, seedDepth);\n  local.x += (seedDepth - 0.5) * (1.0 - abs(local.y)) * 0.16;\n\n  var side = cross(vec3f(0.0, 0.0, 1.0), direction);\n  let sideLengthSquared = dot(side, side);\n  if (sideLengthSquared > 0.0001) {\n    side *= inverseSqrt(sideLengthSquared);\n  } else {\n    side = vec3f(1.0, 0.0, 0.0);\n  }\n  let facing = cross(direction, side);\n  let rollDirection = mix(-1.5, 1.7, seedDepth);\n  let roll = seedLane * 6.28318530718 + view.viewport.w * rollDirection * view.effects.x * 2.4;\n  let rollSin = sin(roll);\n  let rollCos = cos(roll);\n  let bankedSide = side * rollCos + facing * rollSin;\n  let bankedFacing = facing * rollCos - side * rollSin;\n  let depthScale = mix(0.56, 1.58, clamp(renderPosition.z * 0.62 + 0.5, 0.0, 1.0));\n  let scaleShape = 0.46 + seedScale * 0.58 + pow(seedScale, 12.0) * 1.55;\n  let size = view.viewport.y * scaleShape * depthScale * (1.0 - view.gather.z * 0.3);\n  let width = size * 0.72;\n  let lengthScale = size * 1.26 * view.effects.z;\n  let world = renderPosition\n    + direction * local.y * lengthScale\n    + bankedSide * local.x * width\n    + bankedFacing * local.z * width;\n\n  let perspective = 1.0 / max(0.62, 1.0 - world.z * 0.34);\n  let ndc = world.xy * view.viewport.z / vec2f(aspect, 1.0) * perspective;\n  let depth = clamp(0.56 - world.z * 0.24, 0.03, 0.97);\n  let triangle = vertexIndex / 3u;\n  let corner = vertexIndex % 3u;\n  var mapped = vec3f(0.0);\n  var mappedCrease = vec3f(0.0);\n  var shardAlpha = 0.0;\n\n  if (corner == 0u) {\n    let facetSide = select(-1.0, 1.0, triangle == 1u);\n    let localNormal = vec3f(facetSide * 0.394903, 0.0, 0.918723);\n    let normal = bankedSide * localNormal.x + bankedFacing * localNormal.z;\n    let viewDirection = normalize(vec3f(-renderPosition.xy * 0.08, 1.0));\n    let pointerShift = vec2f(view.light.w, view.shape.w);\n    let keyDirection = view.light.xyz;\n    let halfDirection = normalize(keyDirection + viewDirection);\n    let roughness = clamp(view.material.x, 0.04, 0.96);\n    let materialKind = view.material.y;\n    let glow = view.material.w;\n    let reflection = reflect(-viewDirection, normal);\n    let broad = softbox(\n      reflection,\n      vec2f(-0.34, 0.28) + pointerShift * 0.36,\n      vec2f(0.52, 0.22) + roughness * 0.3,\n    );\n    let strip = softbox(\n      reflection,\n      vec2f(0.48, -0.08) - pointerShift * 0.2,\n      vec2f(0.12, 0.72),\n    );\n    let diffuse = max(dot(normal, keyDirection), 0.0);\n    let specularPower = mix(92.0, 9.0, roughness);\n    let specular = pow(max(dot(normal, halfDirection), 0.0), specularPower);\n    let fresnelBase = 1.0 - max(dot(normal, viewDirection), 0.0);\n    let fresnelSquared = fresnelBase * fresnelBase;\n    let fresnel = fresnelSquared * fresnelSquared;\n    let facet = mix(0.76, 1.0, smoothstep(-0.08, 0.08, normal.x));\n    let depthFog = smoothstep(-0.68, 0.58, renderPosition.z);\n    let depthTint = mix(view.accentColor.rgb * 0.52, view.baseColor.rgb, depthFog);\n    var color = depthTint * (0.1 + diffuse * 0.3) * facet;\n    color += view.highlightColor.rgb * (broad * mix(0.3, 0.86, 1.0 - roughness)) * (1.0 + glow * 0.14);\n    color += view.accentColor.rgb * strip * (0.12 + fresnel * 0.42);\n    color += view.highlightColor.rgb * specular * mix(0.82, 1.0, seedDepth);\n    color += mix(view.baseColor.rgb, view.accentColor.rgb, seedLane) * fresnel * (0.15 + glow * 0.16);\n    color += view.accentColor.rgb * (broad * 0.045 + fresnel * 0.075) * glow;\n    var creaseColor = color + view.highlightColor.rgb * (0.08 + specular * 0.22);\n\n    if (materialKind > 0.5 && materialKind < 1.5) {\n      let materialLight = view.highlightColor.rgb * (broad + specular) * 0.32;\n      color = color * 1.1 + materialLight;\n      creaseColor = creaseColor * 1.1 + materialLight;\n    } else if (materialKind >= 1.5) {\n      let satinColor = view.baseColor.rgb * (0.46 + diffuse * 0.46);\n      color = mix(color, satinColor, 0.56);\n      creaseColor = mix(creaseColor, satinColor, 0.56);\n    }\n\n    // A light page acts as a broad fill light, keeping shaded facets in the chosen palette.\n    let fill = mix(view.accentColor.rgb, view.baseColor.rgb, depthFog)\n      * (0.38 + diffuse * 0.12) * facet * view.environment.x;\n    color += fill;\n    creaseColor += fill;\n\n    // Reuse the displacement wave, so the accent catches each facet as the ripple arrives.\n    let pulseColor = mix(view.accentColor.rgb, view.highlightColor.rgb, 0.18);\n    color += pulseColor * rippleLight * (0.85 + fresnel * 0.45);\n    creaseColor += pulseColor * rippleLight * 1.35;\n\n    let fog = mix(0.42, 1.0, depthFog);\n    let exposure = fog * view.material.z * view.effects.w;\n    mapped = aces(color * exposure);\n    mappedCrease = aces(creaseColor * exposure);\n    shardAlpha = mix(0.58, 0.97, depthFog);\n    // Open path endpoints can cross the viewport while morphing. Taper only that moving seam.\n    let seam = smoothstep(0.0, 0.035, path.phase) * (1.0 - smoothstep(0.965, 1.0, path.phase));\n    shardAlpha *= mix(1.0, seam, view.transport.y * view.formation.x * (1.0 - view.gather.z));\n  }\n\n  var out: VertexOut;\n  out.position = vec4f(ndc, depth, 1.0);\n  out.baseAlpha = vec4f(mapped, shardAlpha);\n  out.creaseColor = mappedCrease - mapped;\n  out.localCoord = shapeLocal.xy;\n  return out;\n}\n\n@fragment\nfn fs_main(in: VertexOut) -> @location(0) vec4f {\n  let crease = (1.0 - smoothstep(0.015, 0.11, abs(in.localCoord.x)))\n    * (1.0 - smoothstep(0.78, 1.0, abs(in.localCoord.y)));\n  var coverage = 1.0;\n  if (view.effects.y > 0.001) {\n    let diamondDistance = 1.0 - abs(in.localCoord.y) - abs(in.localCoord.x) / 0.72;\n    let edgeWidth = max(fwidth(diamondDistance) * view.effects.y, 0.0001);\n    coverage = smoothstep(0.0, edgeWidth, diamondDistance);\n  }\n  let mapped = in.baseAlpha.rgb + in.creaseColor * crease;\n  let coveredAlpha = in.baseAlpha.a * coverage;\n  return vec4f(mapped * coveredAlpha, coveredAlpha);\n}\n`;\n\nconst BLOOM_SHADER = `\nstruct PostParams {\n  viewport: vec4f,\n  bloomInfo: vec4f,\n  finishing: vec4f,\n  background: vec4f,\n  temporal: vec4f,\n  tint: vec4f,\n}\n\n@group(0) @binding(0) var sceneTexture: texture_2d<f32>;\n@group(0) @binding(1) var sceneSampler: sampler;\n@group(0) @binding(2) var<uniform> post: PostParams;\n\nfn visibleResidual(uv: vec2f) -> vec4f {\n  let scene = textureSampleLevel(sceneTexture, sceneSampler, uv, 0.0).rgb;\n  let residual = scene - post.background.rgb;\n  let energy = dot(abs(residual), vec3f(0.2126, 0.7152, 0.0722));\n  let threshold = post.bloomInfo.z;\n  let knee = post.bloomInfo.w;\n  let contribution = smoothstep(threshold - knee, threshold + knee, energy);\n  let coverage = energy * contribution;\n  // Store a premultiplied palette halo on light surfaces, never negative radiance.\n  return vec4f(mix(residual * contribution, post.tint.rgb * coverage, post.finishing.w), coverage);\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n  let offset = post.bloomInfo.xy * 0.25;\n  var glow = visibleResidual(uv + offset);\n  glow += visibleResidual(uv - offset);\n  glow += visibleResidual(uv + vec2f(offset.x, -offset.y));\n  glow += visibleResidual(uv + vec2f(-offset.x, offset.y));\n  return glow * 0.25;\n}\n`;\n\nconst BLOOM_BLUR_SHADER = `\nstruct BlurParams {\n  direction: vec4f,\n}\n\n@group(0) @binding(0) var bloomTexture: texture_2d<f32>;\n@group(0) @binding(1) var linearSampler: sampler;\n@group(0) @binding(2) var<uniform> blur: BlurParams;\n\n@fragment\nfn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n  let nearOffset = blur.direction.xy * 1.3846153846;\n  let farOffset = blur.direction.xy * 3.2307692308;\n  var color = textureSampleLevel(bloomTexture, linearSampler, uv, 0.0) * 0.2270270270;\n  color += textureSampleLevel(bloomTexture, linearSampler, uv + nearOffset, 0.0) * 0.3162162162;\n  color += textureSampleLevel(bloomTexture, linearSampler, uv - nearOffset, 0.0) * 0.3162162162;\n  color += textureSampleLevel(bloomTexture, linearSampler, uv + farOffset, 0.0) * 0.0702702703;\n  color += textureSampleLevel(bloomTexture, linearSampler, uv - farOffset, 0.0) * 0.0702702703;\n  return color;\n}\n`;\n\n// Compact 5×7 glyphs keep ASCII self-contained: no font downloads, canvas atlas, or readbacks.\n// Space, punctuation, directional strokes, and dense glyphs cover the six shape samples.\nconst ASCII_GLYPHS = [\n  [0, 0, 0, 0, 0, 0, 0], // space\n  [0, 0, 0, 0, 0, 12, 12], // .\n  [0, 12, 12, 0, 12, 12, 0], // :\n  [0, 0, 0, 31, 0, 0, 0], // -\n  [0, 0, 31, 0, 31, 0, 0], // =\n  [4, 4, 4, 4, 4, 4, 4], // |\n  [1, 2, 2, 4, 8, 8, 16], // /\n  [16, 8, 8, 4, 2, 2, 1], // backslash\n  [0, 4, 4, 31, 4, 4, 0], // +\n  [0, 21, 14, 31, 14, 21, 0], // *\n  [0, 17, 10, 4, 10, 17, 0], // x\n  [2, 4, 8, 16, 8, 4, 2], // <\n  [8, 4, 2, 1, 2, 4, 8], // >\n  [3, 4, 8, 8, 8, 4, 3], // (\n  [24, 4, 2, 2, 2, 4, 24], // )\n  [0, 0, 14, 17, 17, 14, 0], // o\n  [14, 17, 17, 17, 17, 17, 14], // O\n  [10, 10, 31, 10, 31, 10, 10], // #\n  [14, 17, 23, 21, 23, 16, 14], // @\n  [17, 27, 21, 21, 17, 17, 17], // M\n  [17, 17, 17, 21, 21, 27, 17], // W\n  [14, 17, 17, 31, 17, 17, 17] // A\n];\nconst ASCII_SAMPLES = [\n  [0.28, 0.26],\n  [0.72, 0.14],\n  [0.28, 0.56],\n  [0.72, 0.44],\n  [0.28, 0.86],\n  [0.72, 0.74]\n];\nconst ASCII_SHAPES = ASCII_GLYPHS.map(rows =>\n  ASCII_SAMPLES.map(([cx, cy]) => {\n    let sum = 0;\n    let count = 0;\n    for (let y = 0; y < 28; y += 1) {\n      for (let x = 0; x < 20; x += 1) {\n        if (((x + 0.5) / 20 - cx) ** 2 * 0.36 + ((y + 0.5) / 28 - cy) ** 2 > 0.26 ** 2) continue;\n        sum += (rows[Math.floor(y / 4)] >> (4 - Math.floor(x / 4))) & 1;\n        count += 1;\n      }\n    }\n    return sum / Math.max(count, 1);\n  })\n);\nfor (let sample = 0; sample < 6; sample += 1) {\n  const peak = Math.max(...ASCII_SHAPES.map(shape => shape[sample]));\n  for (const shape of ASCII_SHAPES) shape[sample] /= Math.max(peak, 0.001);\n}\n\nconst STYLE_COMMON = `\nstruct StyleParams {\n  viewport: vec4f,\n  background: vec4f,\n  mode: vec4f,\n}\n@group(0) @binding(0) var sourceTexture: texture_2d<f32>;\n@group(0) @binding(1) var sourceSampler: sampler;\n@group(0) @binding(2) var<uniform> style: StyleParams;\n\nfn sampleSource(pixel: vec2f) -> vec3f {\n  if (any(pixel < vec2f(0.0)) || any(pixel >= style.viewport.xy)) { return style.background.rgb; }\n  return textureSampleLevel(sourceTexture, sourceSampler, pixel / style.viewport.xy, 0.0).rgb;\n}\nfn inkLevel(color: vec3f) -> f32 {\n  // Measure contrast against the chosen background, not black: white stays empty too.\n  return clamp(dot(abs(color - style.background.rgb), vec3f(0.2126, 0.7152, 0.0722)) * 2.4, 0.0, 1.0);\n}\n`;\n\nconst ASCII_CELL_SHADER = `${STYLE_COMMON}\nconst INNER = array<vec2f, 6>(${ASCII_SAMPLES.map(point => `vec2f(${point.join(', ')})`).join(', ')});\nconst OUTER = array<vec2f, 10>(\n  vec2f(0.28, -0.2), vec2f(0.72, -0.2), vec2f(-0.22, 0.25), vec2f(1.22, 0.25),\n  vec2f(-0.22, 0.5), vec2f(1.22, 0.5), vec2f(-0.22, 0.75), vec2f(1.22, 0.75),\n  vec2f(0.28, 1.2), vec2f(0.72, 1.2)\n);\nconst RING = array<vec2f, 6>(vec2f(1.0, 0.0), vec2f(0.5, 0.8660254), vec2f(-0.5, 0.8660254),\n  vec2f(-1.0, 0.0), vec2f(-0.5, -0.8660254), vec2f(0.5, -0.8660254));\nconst SHAPES = array<vec3f, ${ASCII_GLYPHS.length * 2}>(\n  ${ASCII_SHAPES.flatMap(shape => [shape.slice(0, 3), shape.slice(3)])\n    .map(part => `vec3f(${part.map(value => value.toFixed(6)).join(', ')})`)\n    .join(',\\n  ')}\n);\nfn edgeContrast(value: f32, outside: f32) -> f32 {\n  let peak = max(max(value, outside), 0.0001);\n  return value * value / peak;\n}\n@fragment\nfn fs_main(@builtin(position) pixel: vec4f) -> @location(0) vec4f {\n  let base = floor(pixel.xy) * style.viewport.zw;\n  var values: array<f32, 6>;\n  var colorSum = vec3f(0.0);\n  var weightSum = 0.0;\n  for (var i = 0u; i < 6u; i++) {\n    let center = base + INNER[i] * style.viewport.zw;\n    var color = sampleSource(center);\n    for (var tap = 0u; tap < 6u; tap++) {\n      color += sampleSource(center + RING[tap] * style.viewport.w * 0.161);\n    }\n    color /= 7.0;\n    values[i] = inkLevel(color);\n    colorSum += color * values[i];\n    weightSum += values[i];\n  }\n  if (weightSum < 0.025) { return vec4f(style.background.rgb, 0.0); }\n  var edges: array<f32, 10>;\n  for (var i = 0u; i < 10u; i++) { edges[i] = inkLevel(sampleSource(base + OUTER[i] * style.viewport.zw)); }\n  values[0] = edgeContrast(values[0], max(max(edges[0], edges[1]), max(edges[2], edges[4])));\n  values[1] = edgeContrast(values[1], max(max(edges[0], edges[1]), max(edges[3], edges[5])));\n  values[2] = edgeContrast(values[2], max(edges[2], max(edges[4], edges[6])));\n  values[3] = edgeContrast(values[3], max(edges[3], max(edges[5], edges[7])));\n  values[4] = edgeContrast(values[4], max(max(edges[4], edges[6]), max(edges[8], edges[9])));\n  values[5] = edgeContrast(values[5], max(max(edges[5], edges[7]), max(edges[8], edges[9])));\n  let peak = max(max(max(values[0], values[1]), max(values[2], values[3])), max(values[4], values[5]));\n  let gain = 1.0 / max(peak, 0.001);\n  let a = vec3f(values[0], values[1], values[2]);\n  let b = vec3f(values[3], values[4], values[5]);\n  // Normalize shape separately from ink color so thin, dim shards do not all select space.\n  let shapeA = a * sqrt(a * gain) * gain;\n  let shapeB = b * sqrt(b * gain) * gain;\n  var best = 0u;\n  var bestDistance = 100.0;\n  for (var glyph = 0u; glyph < ${ASCII_GLYPHS.length}u; glyph++) {\n    let da = shapeA - SHAPES[glyph * 2u];\n    let db = shapeB - SHAPES[glyph * 2u + 1u];\n    let distance = dot(da, da) + dot(db, db);\n    if (distance < bestDistance) { best = glyph; bestDistance = distance; }\n  }\n  // RGB stores the scene palette; alpha is an exact byte-sized glyph index, not opacity.\n  let ink = style.background.rgb + (colorSum / weightSum - style.background.rgb) * 2.2;\n  return vec4f(clamp(ink, vec3f(0.0), vec3f(1.0)), f32(best) / 255.0);\n}\n`;\n\nconst STYLE_SHADER = `${STYLE_COMMON}\n@group(0) @binding(3) var asciiCells: texture_2d<f32>;\nconst GLYPHS = array<vec2u, ${ASCII_GLYPHS.length}>(\n  ${ASCII_GLYPHS.map(rows => `vec2u(${rows.slice(0, 4).reduce((sum, row, i) => sum + row * 2 ** (i * 5), 0)}u, ${rows.slice(4).reduce((sum, row, i) => sum + row * 2 ** (i * 5), 0)}u)`).join(',\\n  ')}\n);\n// A centered Bayer screen distributes quantization error across a stable 4×4 grid.\nconst THRESHOLDS = array<f32, 16>(\n  0.03125, 0.53125, 0.15625, 0.65625, 0.78125, 0.28125, 0.90625, 0.40625,\n  0.21875, 0.71875, 0.09375, 0.59375, 0.96875, 0.46875, 0.84375, 0.34375\n);\nfn glyphBit(glyph: u32, point: vec2i) -> f32 {\n  if (any(point < vec2i(0)) || point.x >= 5 || point.y >= 7) { return 0.0; }\n  let row = u32(point.y);\n  let bits = select(GLYPHS[glyph].x, GLYPHS[glyph].y, row >= 4u);\n  let shift = (row % 4u) * 5u + 4u - u32(point.x);\n  return f32((bits >> shift) & 1u);\n}\nfn orderedDither(color: vec3f, background: vec3f, threshold: f32) -> vec3f {\n  let residual = color - background;\n  let levels = abs(residual) * 3.0;\n  let quantized = (floor(levels) + step(vec3f(threshold), fract(levels))) / 3.0;\n  return clamp(background + sign(residual) * quantized, vec3f(0.0), vec3f(1.0));\n}\n@fragment\nfn fs_main(@builtin(position) pixel: vec4f) -> @location(0) vec4f {\n  let cellPosition = pixel.xy / style.viewport.zw;\n  let cell = vec2i(floor(cellPosition));\n  if (style.mode.x < 1.5) {\n    let color = sampleSource((vec2f(cell) + 0.5) * style.viewport.zw);\n    let index = u32(cell.x % 4) * 4u + u32(cell.y % 4);\n    return vec4f(orderedDither(color, style.background.rgb, THRESHOLDS[index]), 1.0);\n  }\n  let info = textureLoad(asciiCells, clamp(cell, vec2i(0), vec2i(textureDimensions(asciiCells)) - 1), 0);\n  let glyph = min(u32(round(info.a * 255.0)), ${ASCII_GLYPHS.length - 1}u);\n  // Integrate the compact glyph over each display pixel; keep subpixel strokes visible.\n  let local = fract(cellPosition) * vec2f(6.0, 10.0) - vec2f(0.5, 1.5);\n  let footprint = vec2f(6.0, 10.0) / style.viewport.zw;\n  let low = local - footprint * 0.5;\n  let high = local + footprint * 0.5;\n  let origin = vec2i(floor(low));\n  var coverage = 0.0;\n  for (var y = 0; y < 3; y++) {\n    for (var x = 0; x < 3; x++) {\n      let point = origin + vec2i(x, y);\n      let overlap = max(vec2f(0.0), min(high, vec2f(point + 1)) - max(low, vec2f(point)));\n      coverage += glyphBit(glyph, point) * overlap.x * overlap.y;\n    }\n  }\n  coverage /= footprint.x * footprint.y;\n  return vec4f(mix(style.background.rgb, info.rgb, clamp(coverage, 0.0, 1.0)), 1.0);\n}\n`;\n\nconst FINISH_SHADER = `\nstruct PostParams {\n  viewport: vec4f,\n  bloomInfo: vec4f,\n  finishing: vec4f,\n  background: vec4f,\n  temporal: vec4f,\n  tint: vec4f,\n}\n\n@group(0) @binding(0) var sceneTexture: texture_2d<f32>;\n@group(0) @binding(1) var bloomTexture: texture_2d<f32>;\n@group(0) @binding(2) var linearSampler: sampler;\n@group(0) @binding(3) var<uniform> post: PostParams;\n\nfn hash12(value: vec2f) -> f32 {\n  let p = fract(value * vec2f(0.1031, 0.1030));\n  let mixed = p + dot(p, p.yx + 33.33);\n  return fract((mixed.x + mixed.y) * mixed.x);\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2f, @builtin(position) pixel: vec4f) -> @location(0) vec4f {\n  let background = post.background.rgb;\n  // Scene and output have identical dimensions; never filter the sharp base image.\n  var scene = textureLoad(sceneTexture, vec2i(pixel.xy), 0).rgb;\n\n  if (post.finishing.z > 0.000001) {\n    let aspect = post.viewport.x / max(post.viewport.y, 1.0);\n    let centered = (uv - vec2f(0.5)) * vec2f(aspect, 1.0);\n    let radius = clamp(length(centered) / 0.78, 0.0, 1.0);\n    let radialDirection = centered / max(length(centered), 0.0001);\n    let minResolution = min(post.viewport.x, post.viewport.y);\n    let pixelOffset = radialDirection * (post.finishing.z * minResolution * radius * radius);\n    let uvOffset = pixelOffset * post.viewport.zw;\n    let positive = textureSampleLevel(sceneTexture, linearSampler, uv + uvOffset, 0.0).rgb;\n    let negative = textureSampleLevel(sceneTexture, linearSampler, uv - uvOffset, 0.0).rgb;\n    scene = vec3f(positive.r, scene.g, negative.b);\n  }\n\n  var foreground = scene - background;\n  if (post.finishing.x > 0.0001) {\n    let bloom = textureSampleLevel(bloomTexture, linearSampler, uv, 0.0);\n    // A colored haze remains visible on white; protect the opaque facet colors underneath.\n    let haloMask = 1.0 - smoothstep(0.04, 0.4, length(foreground));\n    let haloOpacity = min(bloom.a * post.finishing.x * 1.8, 0.65) * haloMask;\n    let haloColor = bloom.rgb / max(bloom.a, 0.00001);\n    let lightForeground = mix(foreground, haloColor - background, haloOpacity);\n    foreground = mix(foreground + bloom.rgb * post.finishing.x, lightForeground, post.finishing.w);\n  }\n\n  let signal = smoothstep(0.008, 0.18, length(foreground));\n  if (post.finishing.y > 0.0001) {\n    let grainSeed = floor(post.temporal.x * 60.0);\n    let noise = hash12(floor(pixel.xy) + vec2f(grainSeed, grainSeed * 1.6180339)) - 0.5;\n    foreground += vec3f(noise * post.finishing.y * signal);\n  }\n\n  return vec4f(clamp(background + foreground, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n`;\n\nconst parseColor = (value: string, fallback: string): Color => {\n  const match = /^#?([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(value);\n  const source = match || /^#?([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(fallback)!;\n  return [parseInt(source[1], 16) / 255, parseInt(source[2], 16) / 255, parseInt(source[3], 16) / 255, 1];\n};\n\nconst resolveQuality = (canvas: HTMLCanvasElement): Quality => {\n  const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory || 6;\n  const cores = navigator.hardwareConcurrency || 6;\n  const cssPixels = Math.max(1, canvas.clientWidth * canvas.clientHeight);\n  if (canvas.clientWidth < 640 || memory <= 4 || cores <= 4) return 'low';\n  if (cssPixels <= 360000 && memory >= 8 && cores >= 12) return 'high';\n  return 'medium';\n};\n\nconst resolveDpr = (preset: QualityPreset, canvas: HTMLCanvasElement): number => {\n  const cssPixels = Math.max(1, canvas.clientWidth * canvas.clientHeight);\n  // The budget limits supersampling, never the one-pixel-per-CSS-pixel base image.\n  const budgetDpr = Math.sqrt(preset.supersamplePixels / cssPixels);\n  return Math.max(1, Math.min(window.devicePixelRatio || 1, preset.dpr, budgetDpr));\n};\n\nconst resolveBloomSize = (size: readonly [number, number], qualityLevel = 0): [number, number] => {\n  const bloomScale = BLOOM_SCALES[qualityLevel] ?? BLOOM_SCALES[0];\n  return [Math.max(1, Math.round(size[0] * bloomScale)), Math.max(1, Math.round(size[1] * bloomScale))];\n};\n\nconst createRenderGraph = (\n  gpu: Awaited<ReturnType<typeof init>>,\n  outputSize: readonly [number, number],\n  bloomSize: readonly [number, number] = resolveBloomSize(outputSize)\n) => {\n  const viewParams = uniforms(gpu, {\n    viewport: [1, 0.0132, 1, 0],\n    shape: [1, 1, 0.36, 0],\n    effects: [1, 2, 1, 1.12],\n    composition: [0, 0, 0, 1],\n    transport: [0, 0, 0, 0],\n    formation: [1, 0, 0, 0],\n    gather: [0, 0, 0, 0],\n    pointer: [0, 0, 0.54, 0],\n    shock: [0, 0, 4, 0],\n    shockB: [0, 0, 0, 0],\n    shockC: [0, 0, 0, 0],\n    shockD: [0, 0, 0, 0],\n    material: [0.46, MATERIALS.pearl, 0.92, 0.54],\n    light: [-0.321, 0.49, 0.845, 0],\n    environment: [0, 0, 0, 0],\n    baseColor: [137 / 255, 106 / 255, 189 / 255, 1],\n    highlightColor: mixColor([168 / 255, 85 / 255, 247 / 255, 1], [1, 1, 1, 1], MATERIAL_PRESETS.pearl.highlightMix),\n    accentColor: [168 / 255, 85 / 255, 247 / 255, 1]\n  });\n  const postParams = uniforms(gpu, {\n    viewport: [outputSize[0], outputSize[1], 1 / outputSize[0], 1 / outputSize[1]],\n    bloomInfo: [1 / bloomSize[0], 1 / bloomSize[1], 0.2, 0.12],\n    finishing: [0.5, 0.05, 0.0075, 0],\n    background: [0.071, 0.059, 0.09, 1],\n    temporal: [0, 0, 0, 0],\n    tint: [137 / 255, 106 / 255, 189 / 255, 1]\n  });\n  const shardDraw = draw(gpu, {\n    shader: SHARD_SHADER,\n    vertices: 6,\n    blend: 'premultiplied',\n    cull: 'none',\n    depth: false,\n    label: 'aero-shards-procedural'\n  });\n  shardDraw.set({ view: viewParams });\n  const sceneTarget = target(gpu, {\n    size: outputSize,\n    format: 'rgba8unorm',\n    label: 'aero-shards-scene'\n  });\n  const bloomTarget = target(gpu, {\n    size: bloomSize,\n    format: 'rgba16float',\n    label: 'aero-shards-bloom'\n  });\n  const bloomScratchTarget = target(gpu, {\n    size: bloomSize,\n    format: 'rgba16float',\n    label: 'aero-shards-bloom-scratch'\n  });\n  const linearSampler = sampler(gpu, {\n    minFilter: 'linear',\n    magFilter: 'linear',\n    addressModeU: 'clamp-to-edge',\n    addressModeV: 'clamp-to-edge'\n  });\n  const bloomEffect = effect(gpu, BLOOM_SHADER, {\n    label: 'aero-shards-bloom-prefilter',\n    set: {\n      sceneTexture: sceneTarget,\n      sceneSampler: linearSampler,\n      post: postParams\n    }\n  });\n  const blurParamsX = uniforms(gpu, { direction: [1 / bloomSize[0], 0, 0, 0] });\n  const blurParamsY = uniforms(gpu, { direction: [0, 1 / bloomSize[1], 0, 0] });\n  const bloomBlurX = effect(gpu, BLOOM_BLUR_SHADER, {\n    label: 'aero-shards-bloom-horizontal',\n    set: { bloomTexture: bloomTarget, linearSampler, blur: blurParamsX }\n  });\n  const bloomBlurY = effect(gpu, BLOOM_BLUR_SHADER, {\n    label: 'aero-shards-bloom-vertical',\n    set: { bloomTexture: bloomScratchTarget, linearSampler, blur: blurParamsY }\n  });\n  const finishEffect = effect(gpu, FINISH_SHADER, {\n    label: 'aero-shards-finish',\n    set: {\n      sceneTexture: sceneTarget,\n      bloomTexture: bloomTarget,\n      linearSampler,\n      post: postParams\n    }\n  });\n  // Disabled effects retain only tiny placeholders, not full-resolution render targets.\n  const styleTarget = target(gpu, { size: [1, 1], format: 'rgba8unorm', label: 'aero-shards-style' });\n  const asciiTarget = target(gpu, { size: [1, 1], format: 'rgba8unorm', label: 'aero-shards-ascii-cells' });\n  const styleParams = uniforms(gpu, {\n    viewport: [outputSize[0], outputSize[1], 6, 10],\n    background: [0.071, 0.059, 0.09, 1],\n    mode: [0, 0, 0, 0]\n  });\n  const asciiEffect = effect(gpu, ASCII_CELL_SHADER, {\n    label: 'aero-shards-ascii-match',\n    set: { sourceTexture: sceneTarget, sourceSampler: linearSampler, style: styleParams }\n  });\n  const styleEffect = effect(gpu, STYLE_SHADER, {\n    label: 'aero-shards-style-resolve',\n    set: { sourceTexture: sceneTarget, sourceSampler: linearSampler, style: styleParams, asciiCells: asciiTarget }\n  });\n  return {\n    viewParams,\n    postParams,\n    shardDraw,\n    sceneTarget,\n    bloomTarget,\n    bloomScratchTarget,\n    bloomEffect,\n    blurParamsX,\n    blurParamsY,\n    bloomBlurX,\n    bloomBlurY,\n    finishEffect,\n    styleTarget,\n    asciiTarget,\n    styleParams,\n    asciiEffect,\n    styleEffect,\n    styleSignature: ''\n  };\n};\n\nconst configureStyle = (\n  graph: ReturnType<typeof createRenderGraph>,\n  settings: AeroSettings,\n  outputSize: readonly [number, number],\n  cssSize: readonly [number, number]\n) => {\n  const mode = settings.effect;\n  const signature = [mode, ...outputSize, ...cssSize, ...settings.background].join('|');\n  if (signature === graph.styleSignature) return;\n  graph.styleSignature = signature;\n  const cellWidth = ((mode === EFFECTS.ascii ? 3.6 : 1) * outputSize[0]) / Math.max(cssSize[0], 1);\n  const cellHeight = ((mode === EFFECTS.ascii ? 6 : 1) * outputSize[1]) / Math.max(cssSize[1], 1);\n  graph.styleTarget.resize(mode ? outputSize : [1, 1]);\n  graph.asciiTarget.resize(\n    mode === EFFECTS.ascii\n      ? [Math.max(1, Math.ceil(outputSize[0] / cellWidth)), Math.max(1, Math.ceil(outputSize[1] / cellHeight))]\n      : [1, 1]\n  );\n  graph.styleParams.set({\n    viewport: [outputSize[0], outputSize[1], cellWidth, cellHeight],\n    background: settings.background,\n    mode: [mode, 0, 0, 0]\n  });\n  const source = mode ? graph.styleTarget : graph.sceneTarget;\n  graph.bloomEffect.set({ sceneTexture: source });\n  graph.finishEffect.set({ sceneTexture: source });\n};\n\nconst prepareRenderGraph = async (graph: ReturnType<typeof createRenderGraph>, outputFormat: string) => {\n  await Promise.all([\n    graph.shardDraw.compile({ colors: [outputFormat] }),\n    graph.shardDraw.compile(graph.sceneTarget),\n    graph.bloomEffect.compile(graph.bloomTarget),\n    graph.bloomBlurX.compile(graph.bloomScratchTarget),\n    graph.bloomBlurY.compile(graph.bloomTarget),\n    graph.finishEffect.compile({ colors: [outputFormat] }),\n    graph.asciiEffect.compile(graph.asciiTarget),\n    graph.styleEffect.compile(graph.styleTarget)\n  ]);\n};\n\nexport default function AeroShards({\n  backgroundColor = '#120F17',\n  shardColor = '#896ABD',\n  accentColor = '#A855F7',\n  placement = 'full',\n  flow = 'stream',\n  material = 'pearl',\n  detail = 'balanced',\n  effect = 'none',\n  scale = 1,\n  spread = 1,\n  depth = 1,\n  speed = 1,\n  spin = 1,\n  interaction = 'repel',\n  density = 1.5,\n  shardSize = 1.1,\n  stretch = 1,\n  turbulence = 1,\n  glow = 1,\n  edgeSoftness = 2,\n  bloom = 0.5,\n  grain = 0.05,\n  chromaticAberration = 0.0075,\n  transitionDuration = 1,\n  interactionRadius = 1.5,\n  interactionStrength = 0.5,\n  rippleIntensity = 1,\n  holdToGather = true,\n  paused = false,\n  className = '',\n  onError\n}: AeroShardsProps) {\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const onErrorRef = useRef<AeroShardsProps['onError']>(onError);\n  const settingsRef = useRef<AeroSettings | null>(null);\n  const wakeRef = useRef<() => void>(() => {});\n  const pointerRef = useRef<PointerState>({\n    raw: [0.5, 0.5],\n    position: [0.5, 0.5],\n    velocity: [0, 0],\n    active: 0,\n    presence: 0,\n    presenceVelocity: 0,\n    initialized: false\n  });\n  const ripplesRef = useRef<RippleState[]>(createRipples());\n  const holdRef = useRef(createHold());\n  const [ready, setReady] = useState(false);\n\n  const resolvedMaterial = MATERIAL_PRESETS[material] || MATERIAL_PRESETS.pearl;\n  const resolvedDetail = DETAIL_PRESETS[detail] || DETAIL_PRESETS.balanced;\n  const resolvedEffect = EFFECTS[effect] ?? EFFECTS.none;\n  // Stylized marks need enough screen area to resolve; preserve roughly the same field coverage.\n  const effectDetail = resolvedEffect === EFFECTS.none ? 1 : 0.4;\n  const effectSize = resolvedEffect === EFFECTS.none ? 1 : 1.75;\n  const resolvedScale = clamp(scale, 0.5, 2.5);\n  const resolvedBackground = parseColor(backgroundColor, '#120F17');\n  const resolvedShardColor = parseColor(shardColor, '#896ABD');\n  const resolvedAccentColor = parseColor(accentColor, '#A855F7');\n  const resolvedSpread = clamp(spread, 0.15, 1.1);\n  const resolvedDepth = clamp(depth, 0, 1.25);\n  const resolvedSpeed = clamp(speed, 0, 2);\n  const resolvedSpin = clamp(spin, 0, 2);\n  const resolvedInteraction = INTERACTIONS[interaction] ?? INTERACTIONS.repel;\n  const resolvedDensity = clamp(density, 0.5, 1.5);\n  const resolvedShardSize = clamp(shardSize, 0.5, 1.5);\n  const resolvedStretch = clamp(stretch, 0.6, 1.8);\n  const resolvedTurbulence = clamp(turbulence, 0, 2);\n  const resolvedGlow = clamp(glow, 0, 2);\n  const resolvedEdgeSoftness = clamp(edgeSoftness, 0, 2);\n  const resolvedBloom = clamp(bloom, 0, 3);\n  const resolvedGrain = clamp(grain, 0, 0.12);\n  const resolvedChromaticAberration = clamp(chromaticAberration, 0, 0.01);\n  const resolvedTransitionDuration = clamp(transitionDuration, 0.2, 2);\n  const resolvedInteractionRadius = clamp(interactionRadius, 0.5, 2);\n  const resolvedInteractionStrength = clamp(interactionStrength, 0, 2);\n  const backgroundLuma =\n    resolvedBackground[0] * 0.2126 + resolvedBackground[1] * 0.7152 + resolvedBackground[2] * 0.0722;\n  const lightBackground = clamp((backgroundLuma - 0.58) / 0.24, 0, 1);\n  const lightSurface = lightBackground * lightBackground * (3 - 2 * lightBackground);\n\n  settingsRef.current = {\n    background: resolvedBackground,\n    shard: resolvedShardColor,\n    highlight: mixColor(resolvedAccentColor, [1, 1, 1, 1], resolvedMaterial.highlightMix),\n    accent: resolvedAccentColor,\n    composition: PLACEMENTS[placement] ?? PLACEMENTS.full,\n    flow: FLOWS[flow] ?? FLOWS.stream,\n    material: MATERIALS[material] ?? MATERIALS.pearl,\n    effect: resolvedEffect,\n    detailCount: resolvedDetail.count * resolvedDensity * effectDetail,\n    shardSize: resolvedDetail.size * resolvedShardSize * effectSize,\n    scale: resolvedScale,\n    stretch: resolvedStretch * (1 + Math.min(resolvedSpeed * 0.34, 1.2) * 0.1),\n    speed: resolvedSpeed,\n    spin: resolvedSpin,\n    turbulence: 0.36 * resolvedTurbulence,\n    spread: resolvedSpread,\n    depth: resolvedDepth,\n    roughness: resolvedMaterial.roughness,\n    brightness: resolvedMaterial.brightness,\n    glow: resolvedMaterial.glow * resolvedGlow,\n    edgeSoftness: resolvedEdgeSoftness,\n    bloom: resolvedBloom,\n    grain: resolvedGrain,\n    // Keep RGB separation below the scale of the glyph strokes and dither screen.\n    chromaticAberration: resolvedChromaticAberration * (resolvedEffect === EFFECTS.none ? 1 : 0.2),\n    exposure: 1.12 + (0.96 - 1.12) * lightSurface,\n    lightSurface,\n    transitionDuration: resolvedTransitionDuration,\n    interaction: resolvedInteraction,\n    interactionRadius: (interaction === 'attract' ? 0.27 : 0.18) * resolvedInteractionRadius,\n    interactionStrength: resolvedInteractionStrength,\n    rippleIntensity: clamp(rippleIntensity, 0, 2),\n    holdToGather,\n    paused,\n    signature: [\n      backgroundColor,\n      shardColor,\n      accentColor,\n      placement,\n      flow,\n      material,\n      detail,\n      effect,\n      resolvedScale,\n      resolvedSpread,\n      resolvedDepth,\n      resolvedSpeed,\n      resolvedSpin,\n      interaction,\n      resolvedDensity,\n      resolvedShardSize,\n      resolvedStretch,\n      resolvedTurbulence,\n      resolvedGlow,\n      resolvedEdgeSoftness,\n      resolvedBloom,\n      resolvedGrain,\n      resolvedChromaticAberration,\n      resolvedTransitionDuration,\n      resolvedInteractionRadius,\n      resolvedInteractionStrength,\n      rippleIntensity,\n      holdToGather,\n      paused\n    ].join('|')\n  };\n  const settingsSignature = settingsRef.current.signature;\n  onErrorRef.current = onError;\n\n  useEffect(() => {\n    wakeRef.current();\n  }, [settingsSignature]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const root = rootRef.current;\n    if (!canvas || !root) return;\n    resetPointerMotion(pointerRef.current);\n    ripplesRef.current = createRipples();\n    holdRef.current = createHold();\n\n    let disposed = false;\n    let runtimeFailed = false;\n    let gpu: Awaited<ReturnType<typeof init>> | undefined;\n    let animationFrameId = 0;\n    let timeoutId = 0;\n    let unsubscribeResize: (() => void) | undefined;\n    let unsubscribeGpuError: (() => void) | undefined;\n    let visibilityObserver: IntersectionObserver | undefined;\n    let resizeObserver: ResizeObserver | undefined;\n    let visible = true;\n    let visibilityRatio = 1;\n    let needsRender = true;\n    let interactionDeadline = 0;\n    let settlingDeadline = 0;\n    let bounds = root.getBoundingClientRect();\n    let boundsDirty = false;\n    let resumePending = true;\n    let wakeRenderer = () => {\n      needsRender = true;\n    };\n    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');\n\n    const reportFailure = (error: unknown) => {\n      if (disposed || runtimeFailed) return;\n      runtimeFailed = true;\n      if (animationFrameId) cancelAnimationFrame(animationFrameId);\n      if (timeoutId) window.clearTimeout(timeoutId);\n      resizeObserver?.disconnect();\n      visibilityObserver?.disconnect();\n      unsubscribeResize?.();\n      unsubscribeGpuError?.();\n      const failedGpu = gpu;\n      gpu = undefined;\n      failedGpu?.dispose();\n      const resolved = error instanceof Error ? error : new Error(String(error));\n      onErrorRef.current?.(resolved);\n    };\n\n    const updateBounds = () => {\n      bounds = root.getBoundingClientRect();\n      boundsDirty = false;\n    };\n\n    const pointFromClient = (clientX: number, clientY: number): [number, number] | null => {\n      if (boundsDirty) updateBounds();\n      if (bounds.width <= 0 || bounds.height <= 0) return null;\n      const x = (clientX - bounds.left) / bounds.width;\n      const y = (clientY - bounds.top) / bounds.height;\n      if (x < 0 || x > 1 || y < 0 || y > 1) return null;\n      return [x, y];\n    };\n\n    const updatePointerTarget = (next: [number, number]) => {\n      const pointer = pointerRef.current;\n      if (!pointer.initialized || (!pointer.active && pointer.presence === 0)) {\n        pointer.raw = [...next] as [number, number];\n        pointer.position = [...next] as [number, number];\n        pointer.presence = 0;\n        resetPointerMotion(pointer);\n        pointer.initialized = true;\n      } else {\n        pointer.raw[0] = next[0];\n        pointer.raw[1] = next[1];\n      }\n      pointer.active = 1;\n    };\n\n    const deactivatePointer = () => {\n      pointerRef.current.active = 0;\n      holdRef.current.pointerId = null;\n      const now = performance.now();\n      interactionDeadline = now + 140;\n      settlingDeadline = now + 680;\n      wakeRenderer();\n    };\n\n    const handlePointerMove = (event: PointerEvent) => {\n      const settings = settingsRef.current!;\n      if (!event.isPrimary || !visible || settings.interaction === INTERACTIONS.none) return;\n      const next = pointFromClient(event.clientX, event.clientY);\n      if (!next) {\n        const pointer = pointerRef.current;\n        if (pointer.active || pointer.presence > 0) deactivatePointer();\n        return;\n      }\n      updatePointerTarget(next);\n      const now = performance.now();\n      interactionDeadline = now + 140;\n      settlingDeadline = now + 680;\n      wakeRenderer();\n    };\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const settings = settingsRef.current!;\n      if (!event.isPrimary || event.button !== 0 || !visible || settings.interaction === INTERACTIONS.none) return;\n      // Never hijack links, form controls, or editable content layered above a background.\n      if (\n        event.target instanceof Element &&\n        event.target.closest('a, button, input, textarea, select, [role=\"button\"], [contenteditable=\"true\"]')\n      )\n        return;\n      const next = pointFromClient(event.clientX, event.clientY);\n      if (!next) return;\n      if (!settings.paused && !reduceMotion.matches && settings.speed > 0.0001) {\n        startRipple(ripplesRef.current, next, bounds.width / Math.max(bounds.height, 1));\n        if (settings.holdToGather) {\n          holdRef.current.pointerId = event.pointerId;\n          holdRef.current.elapsed = 0;\n        }\n      }\n      updatePointerTarget(next);\n      const now = performance.now();\n      interactionDeadline = now + 220;\n      settlingDeadline = now + 800;\n      wakeRenderer();\n    };\n\n    const handlePointerEnd = (event: PointerEvent) => {\n      const hold = holdRef.current;\n      if (hold.pointerId === event.pointerId) {\n        hold.pointerId = null;\n        const settings = settingsRef.current!;\n        if (\n          hold.amount > 0.1 &&\n          !settings.paused &&\n          !reduceMotion.matches &&\n          settings.interaction !== INTERACTIONS.none\n        ) {\n          startRipple(\n            ripplesRef.current,\n            pointerRef.current.raw,\n            bounds.width / Math.max(bounds.height, 1),\n            1 + hold.amount * 0.8\n          );\n        }\n        wakeRenderer();\n      }\n      if (event.pointerType !== 'mouse') deactivatePointer();\n    };\n\n    const markBoundsDirty = () => {\n      boundsDirty = true;\n    };\n\n    const handleVisibilityChange = () => {\n      resumePending = true;\n      holdRef.current.pointerId = null;\n      resetPointerMotion(pointerRef.current);\n      wakeRenderer();\n    };\n\n    window.addEventListener('pointermove', handlePointerMove, { passive: true });\n    window.addEventListener('pointerdown', handlePointerDown, { passive: true });\n    window.addEventListener('pointerup', handlePointerEnd, { passive: true });\n    window.addEventListener('pointercancel', deactivatePointer, { passive: true });\n    window.addEventListener('blur', deactivatePointer);\n    window.addEventListener('scroll', markBoundsDirty, { passive: true, capture: true });\n    document.addEventListener('visibilitychange', handleVisibilityChange);\n    window.addEventListener('focus', handleVisibilityChange);\n    reduceMotion.addEventListener('change', handleVisibilityChange);\n\n    visibilityObserver = new IntersectionObserver(\n      entries => {\n        const entry = entries[0];\n        visibilityRatio = entry?.intersectionRatio ?? 1;\n        visible = entry ? entry.isIntersecting && visibilityRatio >= 0.02 : true;\n        if (visible) {\n          resumePending = true;\n        } else {\n          interactionDeadline = 0;\n          settlingDeadline = 0;\n          const pointer = pointerRef.current;\n          pointer.active = 0;\n          pointer.presence = 0;\n          holdRef.current.pointerId = null;\n          resetPointerMotion(pointer);\n        }\n        wakeRenderer();\n      },\n      { threshold: [0, 0.02, 0.25] }\n    );\n    visibilityObserver.observe(root);\n\n    void (async () => {\n      try {\n        setReady(false);\n        const resolvedQuality = resolveQuality(canvas);\n        const preset = QUALITY_PRESETS[resolvedQuality] || QUALITY_PRESETS.medium;\n        gpu = await init({ powerPreference: 'low-power' });\n        if (disposed) return gpu.dispose();\n        unsubscribeGpuError = gpu.onError(reportFailure);\n\n        const outputFormat = (\n          navigator as Navigator & { gpu: { getPreferredCanvasFormat(): string } }\n        ).gpu.getPreferredCanvasFormat();\n        const output = surface(gpu, canvas, {\n          dpr: resolveDpr(preset, canvas),\n          autoResize: false,\n          format: outputFormat\n        });\n        const graph = createRenderGraph(gpu, output.size, resolveBloomSize([canvas.clientWidth, canvas.clientHeight]));\n        await prepareRenderGraph(graph, outputFormat);\n        if (disposed) return;\n\n        let lastSettingsSignature = '';\n        let previousRenderTimestamp = 0;\n        let lastPresentationTimestamp = 0;\n        let nextPresentationTimestamp = 0;\n        let flowDistance = 0;\n        let travelPhase = 0;\n        let grainTime = 0;\n        let firstFrame = true;\n        const placementMotion = createFormation(settingsRef.current!.composition);\n        let layoutWeights = placementMotion.weights;\n        let layoutTransitioning = false;\n        const formation = createFormation(settingsRef.current!.flow);\n        let renderScale = settingsRef.current!.scale;\n        let runtimeQualityLevel = 0;\n        let appliedBloomLevel = 0;\n        let pendingBloomResize = false;\n        let pressureStartedAt = 0;\n        let stableStartedAt = performance.now();\n        let lastQualityChange = 0;\n        let encodeAverage = 0;\n        let renderTimestamp = 0;\n        let previousRafTimestamp = 0;\n        let refreshInterval = 1000 / 60;\n        const refreshSamples = new Float32Array(30);\n        let refreshSampleCount = 0;\n        let refreshSampleIndex = 0;\n\n        const resolveFrameState = (now: number): FrameState => {\n          const pointer = pointerRef.current;\n          const pointerTransitioning = Math.abs(pointer.presence - pointer.active) > 0.004;\n          if (\n            now < interactionDeadline ||\n            pointerTransitioning ||\n            ripplesRef.current.some(ripple => ripple.strength > 0) ||\n            layoutTransitioning ||\n            holdRef.current.pointerId !== null ||\n            holdRef.current.amount > 0 ||\n            formation.weights[settingsRef.current!.flow] < 1 ||\n            Math.abs(settingsRef.current!.scale - renderScale) > 0.001\n          ) {\n            return FRAME_STATES.interactive;\n          }\n          if (now < settlingDeadline) return FRAME_STATES.settling;\n          if (visibilityRatio < 0.25) return FRAME_STATES.partial;\n          return FRAME_STATES.ambient;\n        };\n\n        const resizePostTargets = (qualityLevel = appliedBloomLevel) => {\n          const width = Math.max(1, output.size[0]);\n          const height = Math.max(1, output.size[1]);\n          const bloomSize = resolveBloomSize([canvas.clientWidth, canvas.clientHeight], qualityLevel);\n          graph.sceneTarget.resize([width, height]);\n          graph.bloomTarget.resize(bloomSize);\n          graph.bloomScratchTarget.resize(bloomSize);\n          graph.blurParamsX.set({ direction: [1 / bloomSize[0], 0, 0, 0] });\n          graph.blurParamsY.set({ direction: [0, 1 / bloomSize[1], 0, 0] });\n          graph.postParams.set({\n            viewport: [width, height, 1 / width, 1 / height],\n            bloomInfo: [1 / bloomSize[0], 1 / bloomSize[1], 0.2, 0.12]\n          });\n        };\n\n        const resizeOutput = () => {\n          updateBounds();\n          const dpr = resolveDpr(preset, canvas);\n          const nextSize: [number, number] = [\n            Math.max(1, Math.round(canvas.clientWidth * dpr)),\n            Math.max(1, Math.round(canvas.clientHeight * dpr))\n          ];\n          if (nextSize[0] !== output.size[0] || nextSize[1] !== output.size[1]) output.resize(nextSize);\n          resizePostTargets();\n        };\n\n        unsubscribeResize = output.onResize(() => {\n          resizePostTargets();\n          needsRender = true;\n          wakeRenderer();\n        });\n        resizeObserver = new ResizeObserver(() => {\n          resizeOutput();\n          wakeRenderer();\n        });\n        resizeObserver.observe(canvas);\n\n        const setRuntimeQuality = (nextLevel: number, now: number, frameState: FrameState) => {\n          const clampedLevel = Math.max(0, Math.min(RUNTIME_QUALITY.length - 1, nextLevel));\n          if (clampedLevel === runtimeQualityLevel) return;\n          runtimeQualityLevel = clampedLevel;\n          pressureStartedAt = 0;\n          stableStartedAt = now;\n          lastQualityChange = now;\n          if (frameState === FRAME_STATES.interactive || frameState === FRAME_STATES.settling) {\n            pendingBloomResize = true;\n          } else {\n            appliedBloomLevel = runtimeQualityLevel;\n            pendingBloomResize = false;\n            resizePostTargets();\n          }\n        };\n\n        const renderFrame = (currentFrame: Frame) => {\n          const settings = settingsRef.current!;\n          const frozen = settings.paused || reduceMotion.matches || settings.speed <= 0.0001;\n          const elapsed =\n            resumePending || !previousRenderTimestamp\n              ? 0\n              : Math.min(0.05, Math.max(0, (renderTimestamp - previousRenderTimestamp) / 1000));\n          resumePending = false;\n          previousRenderTimestamp = renderTimestamp;\n          lastSettingsSignature = settings.signature;\n          needsRender = false;\n\n          if (!frozen) {\n            flowDistance += elapsed * settings.speed * 0.34;\n            grainTime += elapsed;\n          }\n          if (frozen) {\n            renderScale = settings.scale;\n          } else {\n            renderScale += (settings.scale - renderScale) * (1 - Math.exp(-elapsed * 12));\n          }\n\n          advanceFormation(placementMotion, settings.composition, elapsed, settings.transitionDuration, frozen);\n          layoutWeights = placementMotion.weights;\n          layoutTransitioning = layoutWeights[settings.composition] !== 1;\n          if (!frozen) {\n            const travelAspect = output.size[0] / Math.max(output.size[1], 1);\n            travelPhase =\n              (travelPhase + (elapsed * settings.speed * 0.34) / resolvePathLength(travelAspect, layoutWeights)) % 1;\n          }\n\n          const pointer = pointerRef.current;\n          advanceFormation(formation, settings.flow, elapsed, settings.transitionDuration, frozen);\n          advanceHold(\n            holdRef.current,\n            elapsed,\n            frozen || !settings.holdToGather || settings.interaction === INTERACTIONS.none\n          );\n          if (settings.interaction === INTERACTIONS.none) {\n            pointer.active = 0;\n            pointer.presence = 0;\n            resetPointerMotion(pointer);\n          } else if (frozen) {\n            pointer.position = [...pointer.raw] as [number, number];\n            pointer.presence = pointer.active;\n            resetPointerMotion(pointer);\n          } else if (pointer.initialized) {\n            advancePointer(pointer, elapsed);\n          }\n\n          advanceRipples(ripplesRef.current, elapsed, frozen || settings.interaction === INTERACTIONS.none);\n\n          const runtimeQuality = RUNTIME_QUALITY[runtimeQualityLevel];\n          const activeCount = Math.max(\n            700,\n            Math.round(preset.count * settings.detailCount * runtimeQuality.countScale)\n          );\n          const densityCompensation = Math.pow(1 / runtimeQuality.countScale, 0.2);\n          const shardWorldSize = 0.0125 * settings.shardSize * densityCompensation;\n          const aspect = output.size[0] / Math.max(output.size[1], 1);\n          const lightPresence = settings.interaction === INTERACTIONS.none ? 0 : pointer.presence;\n          const pointerShiftX = (pointer.position[0] - 0.5) * 0.38 * lightPresence;\n          const pointerShiftY = (pointer.position[1] - 0.5) * -0.24 * lightPresence;\n          const lightX = -0.38 + pointerShiftX;\n          const lightY = 0.58 + pointerShiftY;\n          const lightLength = Math.hypot(lightX, lightY, 1);\n          const interactionSign = settings.interaction === INTERACTIONS.attract ? 1 : -1;\n          const interactionPresence =\n            settings.interaction === INTERACTIONS.none\n              ? 0\n              : pointer.presence * settings.interactionStrength * interactionSign * (1 - holdRef.current.amount);\n          const pointerWorldX = ((pointer.position[0] * 2 - 1) * aspect) / renderScale;\n          const pointerWorldY = (1 - pointer.position[1] * 2) / renderScale;\n          const inverseScale = 1 / renderScale;\n          const rippleUniforms = ripplesRef.current.map(ripple => [\n            (ripple.origin[0] * 2 - 1) * aspect * inverseScale,\n            (1 - ripple.origin[1] * 2) * inverseScale,\n            ripple.age,\n            ripple.strength * settings.interactionStrength * settings.rippleIntensity * inverseScale\n          ]);\n\n          graph.viewParams.set({\n            viewport: [aspect, shardWorldSize, renderScale, flowDistance],\n            shape: [settings.spread, settings.depth, settings.turbulence, pointerShiftY],\n            effects: [settings.spin, settings.edgeSoftness, settings.stretch, settings.exposure],\n            composition: layoutWeights,\n            transport: [travelPhase, Math.min(1, (1 - Math.max(...layoutWeights)) * 12), 0, 0],\n            formation: formation.weights,\n            gather: [pointerWorldX, pointerWorldY, holdRef.current.amount, holdRef.current.phase],\n            pointer: [\n              pointerWorldX,\n              pointerWorldY,\n              settings.interactionRadius * 2 * inverseScale,\n              interactionPresence * inverseScale\n            ],\n            shock: rippleUniforms[0],\n            shockB: rippleUniforms[1],\n            shockC: rippleUniforms[2],\n            shockD: rippleUniforms[3],\n            material: [settings.roughness, settings.material, settings.brightness, settings.glow],\n            light: [lightX / lightLength, lightY / lightLength, 1 / lightLength, pointerShiftX],\n            environment: [settings.lightSurface, 0, 0, 0],\n            baseColor: settings.shard,\n            highlightColor: settings.highlight,\n            accentColor: settings.accent\n          });\n          graph.postParams.set({\n            finishing: [settings.bloom, settings.grain, settings.chromaticAberration, settings.lightSurface],\n            background: settings.background,\n            tint: mixColor(settings.shard, settings.accent, 0.4),\n            temporal: [grainTime, 0, 0, 0]\n          });\n\n          configureStyle(graph, settings, output.size, [canvas.clientWidth, canvas.clientHeight]);\n\n          const postEnabled =\n            settings.effect !== EFFECTS.none ||\n            settings.bloom > 0.0001 ||\n            settings.grain > 0.0001 ||\n            settings.chromaticAberration > 0.000001;\n\n          if (!postEnabled) {\n            currentFrame.pass({ target: output, clear: settings.background }, pass => {\n              pass.draw(graph.shardDraw, { instances: activeCount });\n            });\n          } else {\n            currentFrame.pass({ target: graph.sceneTarget, clear: settings.background }, pass => {\n              pass.draw(graph.shardDraw, { instances: activeCount });\n            });\n            if (settings.effect === EFFECTS.ascii) {\n              currentFrame.pass({ target: graph.asciiTarget, clear: [0, 0, 0, 0] }, pass => {\n                pass.draw(graph.asciiEffect);\n              });\n            }\n            if (settings.effect !== EFFECTS.none) {\n              currentFrame.pass({ target: graph.styleTarget, clear: settings.background }, pass => {\n                pass.draw(graph.styleEffect);\n              });\n            }\n            if (settings.bloom > 0.0001) {\n              currentFrame.pass({ target: graph.bloomTarget, clear: [0, 0, 0, 1] }, pass => {\n                pass.draw(graph.bloomEffect);\n              });\n              currentFrame.pass({ target: graph.bloomScratchTarget, clear: [0, 0, 0, 1] }, pass => {\n                pass.draw(graph.bloomBlurX);\n              });\n              currentFrame.pass({ target: graph.bloomTarget, clear: [0, 0, 0, 1] }, pass => {\n                pass.draw(graph.bloomBlurY);\n              });\n            }\n            currentFrame.pass({ target: output, clear: settings.background }, pass => {\n              pass.draw(graph.finishEffect);\n            });\n          }\n\n          if (firstFrame) {\n            firstFrame = false;\n            requestAnimationFrame(() => {\n              if (!disposed) setReady(true);\n            });\n          }\n        };\n\n        const scheduleRaf = () => {\n          if (disposed || runtimeFailed || animationFrameId || !visible || document.hidden) return;\n          animationFrameId = requestAnimationFrame(scheduleFrame);\n        };\n\n        const scheduleSleep = (targetTimestamp: number) => {\n          if (disposed || runtimeFailed || timeoutId || animationFrameId || !visible || document.hidden) return;\n          const delay = Math.max(0, targetTimestamp - performance.now() - 10);\n          timeoutId = window.setTimeout(() => {\n            timeoutId = 0;\n            scheduleRaf();\n          }, delay);\n        };\n\n        const scheduleFrame = (timestamp: number) => {\n          animationFrameId = 0;\n          if (disposed || runtimeFailed || !visible || document.hidden) return;\n\n          if (previousRafTimestamp) {\n            const refreshSample = timestamp - previousRafTimestamp;\n            if (refreshSample > 3 && refreshSample < 35) {\n              refreshSamples[refreshSampleIndex] = refreshSample;\n              refreshSampleIndex = (refreshSampleIndex + 1) % refreshSamples.length;\n              refreshSampleCount = Math.min(refreshSampleCount + 1, refreshSamples.length);\n              refreshInterval = refreshSamples[0];\n              for (let index = 1; index < refreshSampleCount; index += 1) {\n                refreshInterval = Math.min(refreshInterval, refreshSamples[index]);\n              }\n            }\n          }\n          previousRafTimestamp = timestamp;\n\n          const settings = settingsRef.current!;\n          const settingsChanged = settings.signature !== lastSettingsSignature;\n          const frozen = settings.paused || reduceMotion.matches || settings.speed <= 0.0001;\n          if (frozen && !firstFrame && !settingsChanged && !needsRender) return;\n\n          const forceFrame = firstFrame || settingsChanged || !lastPresentationTimestamp;\n          const frameState = resolveFrameState(performance.now());\n          const presentationInterval = resolveFrameInterval(frameState, refreshInterval);\n          const cadenceDeadline = lastPresentationTimestamp + presentationInterval;\n          const dueTimestamp = nextPresentationTimestamp\n            ? Math.min(nextPresentationTimestamp, cadenceDeadline)\n            : cadenceDeadline;\n          if (!forceFrame && timestamp < dueTimestamp - 0.5) {\n            if (frameState.continuous) scheduleRaf();\n            else scheduleSleep(dueTimestamp);\n            return;\n          }\n\n          if (pendingBloomResize && frameState !== FRAME_STATES.interactive && frameState !== FRAME_STATES.settling) {\n            appliedBloomLevel = runtimeQualityLevel;\n            pendingBloomResize = false;\n            resizePostTargets();\n          }\n\n          const sincePresentation = lastPresentationTimestamp ? timestamp - lastPresentationTimestamp : Infinity;\n          renderTimestamp = timestamp;\n          const encodeStart = performance.now();\n          try {\n            frame(gpu!, renderFrame);\n          } catch (error) {\n            reportFailure(error);\n            return;\n          }\n          lastPresentationTimestamp = timestamp;\n\n          const monitorNow = performance.now();\n          const encodeDuration = monitorNow - encodeStart;\n          encodeAverage = encodeAverage ? encodeAverage * 0.9 + encodeDuration * 0.1 : encodeDuration;\n          const missedDeadline = Number.isFinite(sincePresentation) && sincePresentation > presentationInterval * 1.65;\n          const underPressure = encodeAverage > 4 || missedDeadline;\n\n          if (underPressure) {\n            if (!pressureStartedAt) pressureStartedAt = monitorNow;\n          } else {\n            pressureStartedAt = 0;\n          }\n          if (underPressure || frameState !== FRAME_STATES.ambient) stableStartedAt = monitorNow;\n\n          const sustainedPressure = pressureStartedAt > 0 && monitorNow - pressureStartedAt > 1800;\n          const qualityCooldownComplete = monitorNow - lastQualityChange > 2200;\n          if (runtimeQualityLevel < RUNTIME_QUALITY.length - 1 && qualityCooldownComplete && sustainedPressure) {\n            setRuntimeQuality(runtimeQualityLevel + 1, monitorNow, frameState);\n          } else if (\n            runtimeQualityLevel > 0 &&\n            frameState === FRAME_STATES.ambient &&\n            !underPressure &&\n            monitorNow - stableStartedAt > 15000 &&\n            monitorNow - lastQualityChange > 15000\n          ) {\n            setRuntimeQuality(runtimeQualityLevel - 1, monitorNow, frameState);\n          }\n\n          const nextState = resolveFrameState(performance.now());\n          // Carry fractional RAF deadlines so 90/144 Hz displays still average 60 fps.\n          nextPresentationTimestamp = advanceFrameDeadline(\n            timestamp,\n            dueTimestamp,\n            resolveFrameInterval(nextState, refreshInterval),\n            forceFrame || nextState.continuous !== frameState.continuous\n          );\n          if (frozen) return;\n          if (nextState.continuous) scheduleRaf();\n          else scheduleSleep(nextPresentationTimestamp);\n        };\n\n        wakeRenderer = () => {\n          needsRender = true;\n          if (!animationFrameId) nextPresentationTimestamp = 0;\n          if (timeoutId) {\n            window.clearTimeout(timeoutId);\n            timeoutId = 0;\n          }\n          scheduleRaf();\n        };\n        wakeRef.current = wakeRenderer;\n        wakeRenderer();\n      } catch (error) {\n        reportFailure(error);\n      }\n    })();\n\n    return () => {\n      disposed = true;\n      window.removeEventListener('pointermove', handlePointerMove);\n      window.removeEventListener('pointerdown', handlePointerDown);\n      window.removeEventListener('pointerup', handlePointerEnd);\n      window.removeEventListener('pointercancel', deactivatePointer);\n      window.removeEventListener('blur', deactivatePointer);\n      window.removeEventListener('scroll', markBoundsDirty, true);\n      document.removeEventListener('visibilitychange', handleVisibilityChange);\n      window.removeEventListener('focus', handleVisibilityChange);\n      reduceMotion.removeEventListener('change', handleVisibilityChange);\n      visibilityObserver?.disconnect();\n      resizeObserver?.disconnect();\n      unsubscribeResize?.();\n      unsubscribeGpuError?.();\n      if (animationFrameId) cancelAnimationFrame(animationFrameId);\n      if (timeoutId) window.clearTimeout(timeoutId);\n      wakeRef.current = () => {};\n      gpu?.dispose();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`pointer-events-none relative isolate h-full w-full overflow-hidden ${className}`}\n      data-ready={ready}\n      style={{ backgroundColor }}\n      aria-hidden=\"true\"\n    >\n      <canvas\n        ref={canvasRef}\n        className={`pointer-events-none absolute inset-0 block h-full w-full transition-opacity duration-[650ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none ${ready ? 'opacity-100' : 'opacity-0'}`}\n      />\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"vgpu@^0.3.1"
	]
}