{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlyingButton-TS-TW",
	"title": "FlyingButton",
	"description": "Button that fires its thrusters on hover, lifts off on a WebGL smoke plume and lands back in its own cloud.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FlyingButton/FlyingButton.tsx",
			"content": "'use client';\n\nimport {\n  useEffect,\n  useRef,\n  type CSSProperties,\n  type MouseEvent as ReactMouseEvent,\n  type MutableRefObject,\n  type ReactNode\n} from 'react';\n\nconst G = 1100;\nconst SPRING = 380;\nconst DAMP = 10;\nconst REST = G / (2 * SPRING);\nconst POD_H = 5;\nconst MAX_JETS = 4;\nconst TILT_MAX = 0.55;\nconst G_RIDE = G * 0.75;\nconst FLIP_TIME = 0.42;\ntype Size = 'small' | 'medium' | 'large' | 'extra-large';\n\nconst SIZES: Record<Size, { h: number; px: number; fs: number; k: number }> = {\n  small: { h: 36, px: 16, fs: 13, k: 0.82 },\n  medium: { h: 44, px: 22, fs: 14, k: 1 },\n  large: { h: 52, px: 26, fs: 16, k: 1.18 },\n  'extra-large': { h: 60, px: 30, fs: 18, k: 1.36 }\n};\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst num = (v: unknown, d: number) => (Number.isFinite(Number(v)) && v !== null && v !== '' ? Number(v) : d);\nconst smooth = (t: number) => {\n  const s = clamp(t, 0, 1);\n  return s * s * (3 - 2 * s);\n};\nconst jerk = (t: number) => {\n  const s = clamp(t, 0, 1);\n  return s * s * s * (10 + s * (6 * s - 15));\n};\nconst jerkV = (t: number) => {\n  const s = clamp(t, 0, 1);\n  return 30 * s * s * (1 - s) * (1 - s);\n};\nconst jerkA = (t: number) => {\n  const s = clamp(t, 0, 1);\n  return 60 * s * (1 - s) * (1 - 2 * s);\n};\nconst wave = (t: number, k: number) =>\n  Math.sin(t * 1.31 + k) * 0.5 + Math.sin(t * 2.73 + k * 1.7) * 0.3 + Math.sin(t * 5.19 + k * 2.3) * 0.2;\nconst waveD = (t: number, k: number) =>\n  Math.cos(t * 1.31 + k) * 0.655 + Math.cos(t * 2.73 + k * 1.7) * 0.819 + Math.cos(t * 5.19 + k * 2.3) * 1.038;\n\nconst rgb = (hex?: string): [number, number, number] => {\n  const h = String(hex || '').replace('#', '');\n  const v = h.length === 3 ? h.replace(/(.)/g, '$1$1') : h.padEnd(6, '0').slice(0, 6);\n  const n = parseInt(v, 16);\n  if (!Number.isFinite(n)) return [1, 1, 1];\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\nconst VERT = `#version 300 es\nout vec2 vUv;\nvoid main() {\n  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  vUv = p;\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}`;\n\nconst HEAD = `#version 300 es\nprecision highp float;\nprecision highp sampler2D;\nin vec2 vUv;\nout vec4 outColor;\n`;\n\nconst JETS = `\nuniform vec4 uJetA[${MAX_JETS}];\nuniform vec4 uJetB[${MAX_JETS}];\nuniform float uJetBend[${MAX_JETS}];\nfloat jetWeight(vec2 p, vec4 a, vec4 b, out float along) {\n  vec2 rel = p - a.xy;\n  along = dot(rel, a.zw);\n  vec2 off = rel - a.zw * clamp(along, 0.0, b.z);\n  return exp(-dot(off, off) / (b.y * b.y)) * smoothstep(-b.y, 0.0, along);\n}\n`;\n\nconst NOISE = `\nfloat hash(vec2 p) {\n  p = fract(p * vec2(123.34, 456.21));\n  p += dot(p, p + 45.32);\n  return fract(p.x * p.y);\n}\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y);\n}\n`;\n\nconst ADVECT = `${HEAD}\nuniform sampler2D uVelocity;\nuniform sampler2D uSource;\nuniform vec2 uWorld;\nuniform float uDt;\nuniform float uKeep;\nvoid main() {\n  vec2 v = textureLod(uVelocity, vUv, 0.0).xy;\n  if (!(abs(v.x) < 4000.0 && abs(v.y) < 4000.0)) v = vec2(0.0);\n  vec4 c = textureLod(uSource, vUv - uDt * v / uWorld, 0.0) * uKeep;\n  outColor = all(lessThan(abs(c), vec4(1e4))) ? c : vec4(0.0);\n}`;\n\nconst FORCE = `${HEAD}\nuniform sampler2D uVelocity;\nuniform sampler2D uDye;\nuniform vec2 uWorld;\nuniform float uDt;\nuniform float uBuoyancy;\nuniform float uLift;\nuniform float uDamping;\nuniform float uSpread;\nuniform float uSplash;\nuniform float uFriction;\nuniform float uWind;\nuniform vec3 uKick;\nuniform vec4 uCraft;\nuniform vec4 uCraftMotion;\nuniform vec2 uTurb;\nuniform float uTime;\nuniform float uObstacle;\n${JETS}\n${NOISE}\nvec2 curlNoise(vec2 p) {\n  float e = 0.35;\n  float a = noise(p + vec2(0.0, e));\n  float b = noise(p - vec2(0.0, e));\n  float c = noise(p + vec2(e, 0.0));\n  float d = noise(p - vec2(e, 0.0));\n  return vec2(a - b, d - c) / (2.0 * e);\n}\nvoid main() {\n  vec2 p = vUv * uWorld;\n  vec2 v = textureLod(uVelocity, vUv, 0.0).xy;\n  vec2 d = textureLod(uDye, vUv, 1.0).xy;\n  float smokeHere = smoothstep(0.02, 0.4, d.x + d.y);\n  vec2 tq = p / uTurb.y;\n  vec2 turb = curlNoise(tq + vec2(uTime * 0.21, -uTime * 0.37)) + 0.5 * curlNoise(tq * 2.3 + vec2(-uTime * 0.53, uTime * 0.29) + 7.0);\n  v += turb * uTurb.x * smokeHere * uDt;\n  v.y += (uBuoyancy * d.y + uLift * d.x) * uDt;\n  v /= 1.0 + uDamping * uDt;\n  v.x /= 1.0 + uFriction * exp(-p.y / 18.0) * uDt;\n  v.x += (uWind - v.x) * (1.0 - exp(-0.35 * uDt)) * smoothstep(0.0, 60.0, p.y);\n  for (int i = 0; i < ${MAX_JETS}; i++) {\n    vec4 b = uJetB[i];\n    if (b.w <= 0.0) continue;\n    vec4 a = uJetA[i];\n    vec2 rel = p - a.xy;\n    float along = dot(rel, a.zw);\n    float across = dot(rel, vec2(-a.w, a.z));\n    float reach = a.w < -0.2 ? a.y / -a.w : max(b.z, 1.0);\n    float bn = clamp(along / max(reach, 1.0), 0.0, 1.0);\n    across -= uJetBend[i] * bn * bn;\n    vec2 dir = normalize(a.zw + vec2(-a.w, a.z) * (2.0 * uJetBend[i] * bn / max(reach, 1.0)));\n    float sN = clamp(along / max(b.z, 1.0), 0.0, 1.0);\n    float R = b.y + uSpread * clamp(along, 0.0, b.z);\n    float wr = exp(-across * across / (R * R));\n    float wa = smoothstep(-b.y, 0.0, along) * (1.0 - smoothstep(0.55, 1.0, sN));\n    float rate = mix(46.0, 7.0, sN);\n    float w = (1.0 - exp(-uDt * rate)) * wr * wa * clamp(b.w * 3.0, 0.0, 1.0);\n    float U = b.x * sqrt(b.y / R);\n    v = mix(v, dir * U, w);\n  }\n  for (int i = 0; i < ${MAX_JETS}; i++) {\n    vec4 b = uJetB[i];\n    if (b.w <= 0.0) continue;\n    vec4 a = uJetA[i];\n    if (a.w > -0.2) continue;\n    float reach = a.y / -a.w;\n    float gx = a.x + a.z * reach - a.w * uJetBend[i];\n    float near = exp(-reach / uKick.x);\n    float sx = uKick.y + reach * 0.25;\n    float kx = (p.x - gx) / (sx * 1.6);\n    float ky = p.y / (uKick.z * 1.5);\n    float k = exp(-kx * kx - ky * ky);\n    float side = clamp((p.x - gx) / sx, -1.0, 1.0);\n    v += uDt * uSplash * b.w * near * k * vec2(side * 0.7, 0.9);\n  }\n  vec2 q = p - uCraft.xy;\n  float c = cos(uCraftMotion.z);\n  float s = sin(uCraftMotion.z);\n  vec2 lq = vec2(c * q.x + s * q.y, -s * q.x + c * q.y);\n  vec2 e = abs(lq) - uCraft.zw + uCraft.w;\n  float sd = length(max(e, 0.0)) + min(max(e.x, e.y), 0.0) - uCraft.w;\n  float inside = smoothstep(2.0, -2.0, sd) * uObstacle;\n  vec2 cv = uCraftMotion.xy + uCraftMotion.w * vec2(-q.y, q.x);\n  v = mix(v, cv, inside);\n  float speed = length(v);\n  if (!(speed < 1e5)) v = vec2(0.0);\n  else if (speed > 2600.0) v *= 2600.0 / speed;\n  outColor = vec4(v, 0.0, 1.0);\n}`;\n\nconst CURL = `${HEAD}\nuniform sampler2D uVelocity;\nuniform vec2 uTexel;\nvoid main() {\n  float L = textureLod(uVelocity, vUv - vec2(uTexel.x, 0.0), 0.0).y;\n  float R = textureLod(uVelocity, vUv + vec2(uTexel.x, 0.0), 0.0).y;\n  float B = textureLod(uVelocity, vUv - vec2(0.0, uTexel.y), 0.0).x;\n  float T = textureLod(uVelocity, vUv + vec2(0.0, uTexel.y), 0.0).x;\n  outColor = vec4(0.5 * (R - L - T + B), 0.0, 0.0, 1.0);\n}`;\n\nconst VORTICITY = `${HEAD}\nuniform sampler2D uVelocity;\nuniform sampler2D uCurl;\nuniform vec2 uTexel;\nuniform float uStrength;\nuniform float uDt;\nvoid main() {\n  float L = textureLod(uCurl, vUv - vec2(uTexel.x, 0.0), 0.0).x;\n  float R = textureLod(uCurl, vUv + vec2(uTexel.x, 0.0), 0.0).x;\n  float B = textureLod(uCurl, vUv - vec2(0.0, uTexel.y), 0.0).x;\n  float T = textureLod(uCurl, vUv + vec2(0.0, uTexel.y), 0.0).x;\n  float C = textureLod(uCurl, vUv, 0.0).x;\n  vec2 f = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));\n  f /= length(f) + 1e-4;\n  f *= uStrength * C;\n  f.y *= -1.0;\n  vec2 v = textureLod(uVelocity, vUv, 0.0).xy + f * uDt;\n  if (!(abs(v.x) < 4000.0 && abs(v.y) < 4000.0)) v = vec2(0.0);\n  outColor = vec4(v, 0.0, 1.0);\n}`;\n\nconst DIVERGENCE = `${HEAD}\nuniform sampler2D uVelocity;\nuniform vec2 uTexel;\nvoid main() {\n  vec2 C = textureLod(uVelocity, vUv, 0.0).xy;\n  float L = textureLod(uVelocity, vUv - vec2(uTexel.x, 0.0), 0.0).x;\n  float R = textureLod(uVelocity, vUv + vec2(uTexel.x, 0.0), 0.0).x;\n  float B = textureLod(uVelocity, vUv - vec2(0.0, uTexel.y), 0.0).y;\n  float T = textureLod(uVelocity, vUv + vec2(0.0, uTexel.y), 0.0).y;\n  if (vUv.y - uTexel.y < 0.0) B = -C.y;\n  outColor = vec4(0.5 * (R - L + T - B), 0.0, 0.0, 1.0);\n}`;\n\nconst PRESSURE_SAMPLE = `\nfloat pressureAt(sampler2D tex, vec2 uv, vec2 texel, float c, int side) {\n  if (side == 0 && uv.x - texel.x < 0.0) return 0.0;\n  if (side == 1 && uv.x + texel.x > 1.0) return 0.0;\n  if (side == 2 && uv.y - texel.y < 0.0) return c;\n  if (side == 3 && uv.y + texel.y > 1.0) return 0.0;\n  vec2 o = side == 0 ? vec2(-texel.x, 0.0) : side == 1 ? vec2(texel.x, 0.0) : side == 2 ? vec2(0.0, -texel.y) : vec2(0.0, texel.y);\n  return textureLod(tex, uv + o, 0.0).x;\n}\n`;\n\nconst PRESSURE = `${HEAD}\nuniform sampler2D uPressure;\nuniform sampler2D uDivergence;\nuniform vec2 uTexel;\n${PRESSURE_SAMPLE}\nvoid main() {\n  float C = textureLod(uPressure, vUv, 0.0).x;\n  float L = pressureAt(uPressure, vUv, uTexel, C, 0);\n  float R = pressureAt(uPressure, vUv, uTexel, C, 1);\n  float B = pressureAt(uPressure, vUv, uTexel, C, 2);\n  float T = pressureAt(uPressure, vUv, uTexel, C, 3);\n  float div = textureLod(uDivergence, vUv, 0.0).x;\n  float pr = (L + R + B + T - div) * 0.25;\n  outColor = vec4(abs(pr) < 1e6 ? pr : 0.0, 0.0, 0.0, 1.0);\n}`;\n\nconst GRADIENT = `${HEAD}\nuniform sampler2D uPressure;\nuniform sampler2D uVelocity;\nuniform vec2 uTexel;\n${PRESSURE_SAMPLE}\nvoid main() {\n  float C = textureLod(uPressure, vUv, 0.0).x;\n  float L = pressureAt(uPressure, vUv, uTexel, C, 0);\n  float R = pressureAt(uPressure, vUv, uTexel, C, 1);\n  float B = pressureAt(uPressure, vUv, uTexel, C, 2);\n  float T = pressureAt(uPressure, vUv, uTexel, C, 3);\n  vec2 v = textureLod(uVelocity, vUv, 0.0).xy - 0.5 * vec2(R - L, T - B);\n  if (vUv.y - uTexel.y < 0.0) v.y = max(v.y, 0.0);\n  if (!(abs(v.x) < 4000.0 && abs(v.y) < 4000.0)) v = vec2(0.0);\n  outColor = vec4(v, 0.0, 1.0);\n}`;\n\nconst BLUR = `${HEAD}\nuniform sampler2D uSource;\nuniform vec2 uStep;\nvoid main() {\n  vec2 c = textureLod(uSource, vUv, 0.0).xy * 0.2270270;\n  c += textureLod(uSource, vUv + uStep * 1.3846154, 0.0).xy * 0.3162162;\n  c += textureLod(uSource, vUv - uStep * 1.3846154, 0.0).xy * 0.3162162;\n  c += textureLod(uSource, vUv + uStep * 3.2307692, 0.0).xy * 0.0702703;\n  c += textureLod(uSource, vUv - uStep * 3.2307692, 0.0).xy * 0.0702703;\n  outColor = vec4(c, 0.0, 1.0);\n}`;\n\nconst SCALE = `${HEAD}\nuniform sampler2D uSource;\nuniform float uScale;\nvoid main() {\n  outColor = textureLod(uSource, vUv, 0.0) * uScale;\n}`;\n\nconst DYE = `${HEAD}\nuniform sampler2D uVelocity;\nuniform sampler2D uSource;\nuniform vec2 uWorld;\nuniform float uDt;\nuniform vec2 uKeep;\nuniform vec4 uRate;\nuniform vec4 uCol;\nuniform vec3 uKick;\nuniform float uDisperse;\nuniform float uDispV;\nuniform vec4 uCraft;\nuniform vec4 uCraftMotion;\nuniform vec2 uGap;\nuniform float uJetX[${MAX_JETS}];\nuniform float uJetN;\nuniform float uFloor;\nuniform float uTime;\n${JETS}\n${NOISE}\nvoid main() {\n  vec2 v = textureLod(uVelocity, vUv, 0.0).xy;\n  vec2 val = textureLod(uSource, vUv - uDt * v / uWorld, 0.0).xy * uKeep;\n  val.x *= exp(-uDisperse * max(length(v) - uDispV, 0.0) * 0.01 * uDt);\n  {\n    vec2 q = vUv * uWorld - uCraft.xy;\n    float c = cos(uCraftMotion.z);\n    float sn = sin(uCraftMotion.z);\n    vec2 lq = vec2(c * q.x + sn * q.y, -sn * q.x + c * q.y);\n    float belly = uCraft.y - uCraft.w - 7.0;\n    float depth = 0.68 * belly * smoothstep(40.0, 110.0, belly);\n    float below = -lq.y - uCraft.w;\n    float wy = smoothstep(-4.0, 6.0, below) * (1.0 - smoothstep(depth * 0.7, depth, below));\n    float nearest = 1e4;\n    float outer = 0.0;\n    for (int k = 0; k < ${MAX_JETS}; k++) {\n      if (float(k) >= uJetN) break;\n      nearest = min(nearest, abs(lq.x - uJetX[k]));\n      outer = max(outer, abs(uJetX[k]));\n    }\n    float wx = uGap.x > 1.0 ? smoothstep(uGap.x * 0.22, uGap.x * 0.4, nearest) * (1.0 - smoothstep(outer - uGap.x * 0.1, outer, abs(lq.x))) : 0.0;\n    val.x *= exp(-uGap.y * wx * wy * uDt);\n  }\n  val.x = max(val.x - uFloor * uDt, 0.0);\n  vec2 p = vUv * uWorld;\n  for (int i = 0; i < ${MAX_JETS}; i++) {\n    vec4 jb = uJetB[i];\n    if (jb.w <= 0.0) continue;\n    vec4 ja = uJetA[i];\n    vec2 rel = p - ja.xy;\n    float along = dot(rel, ja.zw);\n    float across = dot(rel, vec2(-ja.w, ja.z));\n    vec2 off = rel - ja.zw * clamp(along, 0.0, 6.0);\n    float wn = exp(-dot(off, off) / (jb.y * jb.y)) * smoothstep(-jb.y, 0.0, along);\n    float pulse = 0.6 + 0.8 * noise(vec2(uTime * 6.0 + float(i) * 13.1, along * 0.06 + float(i)));\n    val.x += uRate.x * jb.w * wn * pulse * uDt;\n    val.y += uRate.y * jb.w * wn * uDt;\n    float reach = ja.w < -0.2 ? ja.y / -ja.w : 1e4;\n    float sN = clamp(along / max(reach, 1.0), 0.0, 1.0);\n    across -= uJetBend[i] * sN * sN;\n    float R = uCol.y + uCol.x * clamp(along, 0.0, reach);\n    float wc = exp(-across * across / (R * R)) * smoothstep(0.45, 0.8, sN) * (1.0 - step(reach + 2.0, along));\n    float grain = 0.45 + 1.1 * noise(vec2(across * 0.12 + float(i) * 3.0, along * 0.05 - uTime * 9.0));\n    val.x += uRate.w * jb.w * wc * grain * uDt;\n    val.y += uRate.y * 0.25 * jb.w * wc * uDt;\n    if (reach < 1e3) {\n      float gx = ja.x + ja.z * reach - ja.w * uJetBend[i];\n      float near = exp(-reach / uKick.x);\n      float spread = uKick.y + reach * 0.25;\n      float kx = (p.x - gx) / spread;\n      float ky = p.y / uKick.z;\n      float k = exp(-kx * kx - ky * ky);\n      float g2 = 0.5 + noise(vec2(p.x * 0.08, uTime * 4.0 + float(i) * 9.0));\n      val.x += uRate.z * jb.w * near * k * g2 * uDt;\n      val.y += uRate.y * 0.4 * jb.w * near * k * uDt;\n    }\n  }\n  if (!(val.x < 1e3 && val.y < 1e3 && val.x >= 0.0 && val.y >= 0.0)) val = vec2(0.0);\n  outColor = vec4(val, 0.0, 1.0);\n}`;\n\nconst RENDER = `${HEAD}\nuniform sampler2D uDye;\nuniform vec2 uWorld;\nuniform vec2 uDyeSize;\nuniform float uTime;\nuniform float uLightTheme;\nuniform vec3 uSmoke;\nuniform vec3 uShade;\nuniform vec3 uThrust;\nuniform vec4 uLook;\nuniform vec4 uLook2;\nuniform vec4 uLook3;\nuniform vec4 uCol;\nuniform float uGamma;\nuniform float uTone;\nuniform vec3 uLight2;\nuniform float uThreshold;\nuniform float uScale;\nuniform vec4 uCraft;\nuniform vec4 uCraftMotion;\n${JETS}\n${NOISE}\nfloat dens(vec2 p, float lod) {\n  return textureLod(uDye, p / uWorld, lod).x;\n}\nfloat hash1(float n) {\n  return fract(sin(n * 127.1) * 43758.5453);\n}\nfloat fbm2(vec2 p) {\n  return noise(p) * 0.65 + noise(p * 2.13 + 17.0) * 0.35;\n}\nfloat flowNoise(float q, float xn, float t, float id, float rate) {\n  float c = t / 0.7;\n  float f1 = fract(c);\n  float f2 = fract(c + 0.5);\n  float k1 = floor(c);\n  float k2 = floor(c + 0.5);\n  float n1 = fbm2(vec2(q - f1 * 0.7 * rate + hash1(k1 + id * 7.0) * 41.0, xn * 1.15 + id * 3.7));\n  float n2 = fbm2(vec2(q - f2 * 0.7 * rate + hash1(k2 + id * 7.0 + 0.5) * 41.0, xn * 1.15 + id * 3.7 + 11.0));\n  float w1 = 1.0 - abs(2.0 * f1 - 1.0);\n  float w2 = 1.0 - w1;\n  return 0.5 + (n1 * w1 + n2 * w2 - 0.5) / sqrt(w1 * w1 + w2 * w2);\n}\nfloat column(vec2 p, vec4 a, float str, float id, float t, float bend, out float xn, out float s) {\n  vec2 rel = p - a.xy;\n  s = dot(rel, a.zw);\n  float x = dot(rel, vec2(-a.w, a.z));\n  float S = a.w < -0.2 ? a.y / -a.w : 1e4;\n  float bn = clamp(s / max(S, 1.0), 0.0, 1.0);\n  x -= bend * bn * bn;\n  xn = 0.0;\n  if (s < -4.0 || s > S + 6.0) return 0.0;\n  float sc = clamp(s, 0.0, S);\n  float R = uCol.y + uCol.x * sc;\n  float flare = smoothstep(S - 32.0 * uScale, S + 2.0, sc);\n  R *= 1.0 + 1.3 * flare * flare;\n  xn = x / R;\n  if (abs(xn) > 3.2) return 0.0;\n  float q = log(1.0 + 0.22 * sc / (9.0 * uScale)) / 0.22;\n  float lam = 9.0 * uScale + 0.22 * sc;\n  float U = uCol.w * sqrt(str) * sqrt(uCol.y / R);\n  float n = flowNoise(q, xn, t, id, U / lam);\n  float amp = 0.12 + 0.5 * smoothstep(4.0, 70.0, sc);\n  float u = abs(xn) * (1.0 - amp * (n - 0.5) * 1.8);\n  float prof = exp(-u * u * 1.7);\n  float dil = pow(uCol.y / R, 0.5);\n  float onset = smoothstep(2.0 * uScale, 16.0 * uScale, s);\n  float tail = 1.0 - smoothstep(S - 2.0, S + 6.0, s);\n  return uCol.z * str * prof * dil * onset * tail * (0.7 + 0.6 * n);\n}\nfloat shape(vec2 p, float lod) {\n  return log(1.0 + 3.0 * dens(p, lod));\n}\nvoid main() {\n  vec2 p = vUv * uWorld;\n  float texel = uWorld.x / uDyeSize.x;\n  float D = textureLod(uDye, vUv, uLook.w).x;\n  float heat = textureLod(uDye, vUv, 0.5).y;\n\n  float e = uLook3.x;\n  float lodN = max(log2(e / texel), 0.0) + uLook3.y;\n  vec2 g = vec2(shape(p + vec2(e, 0.0), lodN) - shape(p - vec2(e, 0.0), lodN), shape(p + vec2(0.0, e), lodN) - shape(p - vec2(0.0, e), lodN)) / (2.0 * e);\n  float e2 = e * 3.0;\n  float lodB = lodN + 1.6;\n  vec2 g2 = vec2(shape(p + vec2(e2, 0.0), lodB) - shape(p - vec2(e2, 0.0), lodB), shape(p + vec2(0.0, e2), lodB) - shape(p - vec2(0.0, e2), lodB)) / (2.0 * e2);\n  vec3 nS = normalize(vec3(-(g * uLook3.z + g2 * uLook3.w) * uLook.y, 1.0));\n\n  float colD = 0.0;\n  vec2 colN = vec2(0.0);\n  float colHot = 0.0;\n  float colNear = 0.0;\n  for (int i = 0; i < ${MAX_JETS}; i++) {\n    vec4 jb = uJetB[i];\n    if (jb.w <= 0.0) continue;\n    vec4 ja = uJetA[i];\n    float xn;\n    float sa;\n    float cd = column(p, ja, min(jb.w, 1.6), float(i), uTime, uJetBend[i], xn, sa);\n    colD += cd;\n    colN += vec2(-ja.w, ja.z) * clamp(xn, -1.6, 1.6) * cd;\n    colHot += cd * exp(-max(sa, 0.0) / 45.0);\n    colNear += cd * exp(-max(sa, 0.0) / 16.0);\n  }\n  float Dt = D + colD;\n  float alpha = 1.0 - exp(-uLook.x * pow(max(D - uThreshold, 0.0) + colD, uGamma));\n  vec2 cn = colD > 1e-5 ? colN / colD : vec2(0.0);\n  vec3 nC = normalize(vec3(cn * 0.85, sqrt(max(1.0 - dot(cn, cn) * 0.72, 0.08))));\n  float wC = colD / max(Dt, 1e-5);\n  vec3 n = normalize(mix(nS, nC, wC));\n  vec3 L = normalize(vec3(-0.38, 0.6, 0.72));\n  float diff = clamp((dot(n, L) + uLook2.z) / (1.0 + uLook2.z), 0.0, 1.0);\n\n  vec2 ld = normalize(L.xy);\n  float tau = 0.0;\n  float dist = 2.0;\n  float stepLen = 3.0;\n  for (int i = 0; i < 9; i++) {\n    float lod = max(log2(stepLen / texel), 0.0);\n    tau += dens(p + ld * dist, lod) * stepLen;\n    dist += stepLen;\n    stepLen *= 1.4;\n  }\n  float sun = mix(exp(-uLook.z * tau), 1.0, wC * 0.6);\n  float ao = mix(exp(-uLook2.w * dens(p, 4.5)), 1.0, wC);\n\n  vec3 amb = vec3(0.84, 0.88, 1.0) * uLook2.x;\n  vec3 key = vec3(1.0, 0.985, 0.96) * uLook2.y;\n  vec3 lit = amb * ao + key * sun * diff;\n\n  vec3 jl = vec3(0.0);\n  vec3 core = vec3(0.0);\n  for (int i = 0; i < ${MAX_JETS}; i++) {\n    vec4 jb = uJetB[i];\n    if (jb.w <= 0.0) continue;\n    vec4 ja = uJetA[i];\n    vec2 rel = p - ja.xy;\n    float along = dot(rel, ja.zw);\n    float across = dot(rel, vec2(-ja.w, ja.z));\n    float str = min(jb.w, 1.6);\n    float flick = 0.9 + 0.2 * noise(vec2(uTime * 38.0, float(i) * 5.0));\n    float len = (8.0 + 12.0 * str) * flick * uScale;\n    float sN = along / len;\n    float wid = 3.2 * uScale * (0.9 - 0.5 * clamp(sN, 0.0, 1.0)) + 0.3;\n    float radial = exp(-across * across / (wid * wid));\n    float axial = smoothstep(-0.12, 0.04, sN) * pow(clamp(1.0 - sN, 0.0, 1.0), 1.5);\n    float c = radial * axial * str;\n    core += mix(uThrust, vec3(1.0), smoothstep(0.25, 0.9, c)) * c * 1.3;\n    float d = length(rel - ja.zw * clamp(along, 0.0, len));\n    core += uThrust * str * (0.22 * exp(-d / (5.0 * uScale)) + 0.06 * exp(-d / (18.0 * uScale)));\n    jl += uThrust * str * uLight2.x * (exp(-d / (30.0 * uScale)) + 0.45 * exp(-d / (100.0 * uScale)));\n  }\n\n  vec3 jlNear = jl * 0.35 + uThrust * colNear / max(Dt, 1e-4) * 0.9;\n  jl += uThrust * colHot / max(Dt, 1e-4) * 0.9;\n  if (uLightTheme > 0.5) {\n    float shade = dot(lit + jl * 0.8, vec3(0.3333)) * uLight2.z;\n    vec3 ink = mix(uShade, uSmoke, smoothstep(0.15, 1.2, shade));\n    alpha = 1.0 - pow(1.0 - alpha, 1.25);\n    ink = mix(ink, uThrust, clamp(dot(jlNear, vec3(0.3333)) * 0.5, 0.0, 0.3));\n    float ca = clamp(max(core.r, max(core.g, core.b)) * 0.85, 0.0, 1.0);\n    vec3 coreInk = mix(uThrust, vec3(1.0), 0.35 * smoothstep(0.7, 1.0, ca));\n    vec3 lc = ink * alpha * (1.0 - ca) + coreInk * ca;\n    float la = alpha + ca * (1.0 - alpha);\n    outColor = vec4(lc, la);\n    return;\n  }\n  vec3 smoke = uSmoke * (lit + jl) * uLight2.z;\n  smoke = mix(smoke / (1.0 + 0.35 * max(smoke - 0.8, 0.0)), 1.0 - exp(-smoke * 1.35), uTone);\n  vec3 emit = mix(uThrust, vec3(1.0), 0.5) * heat * heat * uLight2.y;\n  vec3 col = (smoke + emit) * alpha + core;\n  float a = clamp(max(alpha, max(col.r, max(col.g, col.b))), 0.0, 1.0);\n  col = min(col, vec3(a));\n  col += (hash(gl_FragCoord.xy + fract(uTime) * 61.0) - 0.5) / 255.0 * a;\n  outColor = vec4(max(col, 0.0), a);\n}`;\n\nconst SMOKE = {\n  colSpread: 0.1,\n  colR: 3.6,\n  colD: 1.25,\n  colU: 420,\n  splash: 400,\n  friction: 5,\n  disperse: 0.6,\n  dispV: 60,\n  gap: 10,\n  wind: 8,\n  turb: 350,\n  turbScale: 34,\n  jetLight: 0.3,\n  threshold: 0.035,\n  blur: 3,\n  emit: 0,\n  exposure: 1.3,\n  tone: 1,\n  gamma: 0.6,\n  kickNear: 45,\n  kickW: 18,\n  kickH: 14,\n  vort: 6,\n  spread: 0.1,\n  floor: 0.02,\n  keepD: 0.3,\n  keepIdle: 0.45,\n  keepH: 1.6,\n  rateN: 0,\n  rateH: 6,\n  rateK: 20,\n  rateC: 3,\n  buoy: 330,\n  lift: 22,\n  damp: 0.55,\n  radius: 6,\n  speed: 380,\n  reach: 0.9,\n  forceMax: 240,\n  alphaK: 4,\n  relief: 9,\n  shadow: 0.026,\n  alphaLod: 0,\n  ambient: 0.58,\n  key: 0.82,\n  wrap: 0.35,\n  ao: 0.35,\n  normalE: 3,\n  normalLod: -0.8,\n  fine: 0.35,\n  broad: 0.9,\n  iters: 28\n};\n\ntype Theme = 'dark' | 'light';\ntype Trigger = 'hover' | 'click';\ntype ClickHandler = (event: ReactMouseEvent<HTMLButtonElement> | PointerEvent) => void;\n\ninterface EngineProps {\n  size: Size;\n  altitude: number;\n  hoverTime: number;\n  jets: number;\n  thrusters: boolean;\n  trigger: Trigger;\n  carryCursor: boolean;\n  ramp: boolean;\n  smoke: number;\n  turbulence: number;\n  wobble: number;\n  thrustColor: string;\n  smokeColor: string;\n  theme: Theme;\n  onClick?: ClickHandler;\n}\n\ninterface EngineArgs {\n  root: HTMLDivElement;\n  canvas: HTMLCanvasElement;\n  button: HTMLButtonElement;\n  cursor: SVGSVGElement | null;\n  ramp: SVGSVGElement | null;\n  props: MutableRefObject<EngineProps>;\n}\n\ninterface Engine {\n  refresh: () => void;\n  rehome: () => void;\n  destroy: () => void;\n}\n\ninterface RampGeo {\n  ss: number[];\n  xs: number[];\n  ys: number[];\n  ps: number[];\n  ks: number[];\n  r: number;\n  T: number;\n  x0: number;\n  foot: number;\n  S: number;\n  clear: number;\n  ok: boolean;\n}\n\ninterface Track {\n  geo: RampGeo;\n  base: number;\n  v0: number;\n  acc: number;\n}\n\ninterface Back {\n  from: number;\n  to: number;\n  time: number;\n}\n\ninterface Turn {\n  th: number;\n  w: number;\n}\n\ninterface Piece {\n  el: HTMLElement;\n  x: number;\n  y: number;\n  k: number;\n}\n\ninterface Strip {\n  wrap: HTMLDivElement;\n  ink: HTMLDivElement;\n  parts: HTMLElement[];\n  n: number;\n  sn: number[];\n  pods: Piece[];\n  items: Piece[];\n}\n\ninterface Nozzle {\n  x: number;\n  y: number;\n  dx: number;\n  dy: number;\n}\n\ninterface Target {\n  tex: WebGLTexture | null;\n  fbo: WebGLFramebuffer | null;\n  w: number;\n  h: number;\n  mips: boolean;\n}\n\ninterface Pair {\n  read: Target;\n  write: Target;\n  swap: () => void;\n}\n\ninterface Program {\n  p: WebGLProgram;\n  u: Record<string, WebGLUniformLocation | null>;\n}\n\ntype Uniform = number | number[] | Float32Array | Target | { f1: number[] };\n\ninterface Jets {\n  a: Float32Array;\n  b: Float32Array;\n  bend: number[];\n}\n\ninterface SimShared {\n  jets: Jets;\n  craft: number[];\n  motion: number[];\n  scale: number;\n  activity: number;\n  time: number;\n}\n\ninterface StepInput extends SimShared {\n  jetX: number[];\n  jetN: number;\n  spacing: number;\n  calm: number;\n  wind: number;\n  smoke: number;\n  turbulence: number;\n}\n\ninterface RenderInput extends SimShared {\n  light: boolean;\n  smokeColor: number[];\n  shadeColor: number[];\n  thrustColor: number[];\n}\n\ntype Sim = ReturnType<typeof createSim>;\n\nfunction createEngine({ root, canvas, button, cursor, ramp, props }: EngineArgs): Engine {\n  const gl = canvas.getContext('webgl2', {\n    alpha: true,\n    premultipliedAlpha: true,\n    antialias: false,\n    depth: false,\n    stencil: false,\n    powerPreference: 'high-performance'\n  });\n  const motionQuery =\n    typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;\n  const s = {\n    W: 0,\n    H: 0,\n    dpr: 1,\n    bw: 0,\n    bh: 0,\n    x: 0,\n    vx: 0,\n    y: -REST,\n    vy: 0,\n    th: 0,\n    w: 0,\n    fvx: 0,\n    fvy: 0,\n    thrust: [0, 0, 0, 0],\n    delay: [0, 0, 0, 0],\n    phase: 'rest',\n    pt: 0,\n    clock: 0,\n    seed: Math.random() * 100,\n    wind: 1,\n    alt: 160,\n    climbT: 1.6,\n    y0: 0,\n    vrPrev: 0,\n    hold: 2.4,\n    xRef: 0,\n    landX: 0,\n    boost: false,\n    contact: true,\n    placed: false,\n    track: null as Track | null,\n    back: null as Back | null,\n    flip: null as Turn | null,\n    rampKey: '',\n    rampGeo: null as RampGeo | null,\n    strip: null as Strip | null,\n    u: 0,\n    v: 0,\n    target: { x: 0, y: 0 },\n    pointer: { in: false, x: 0, y: 0, type: 'mouse' },\n    off: { x: 0, y: 0 },\n    attached: false,\n    hover: false,\n    armed: true,\n    carry: false,\n    pressed: false,\n    press: null as { x: number; y: number } | null,\n    drag: null as { gx: number; gy: number } | null,\n    suppress: false,\n    lastC: null as { x: number; y: number } | null,\n    lastTh: 0,\n    quiet: 0,\n    running: false,\n    last: 0,\n    raf: 0,\n    simTime: 0,\n    activity: 0\n  };\n\n  let sim: Sim | null = null;\n  if (gl) {\n    const floatOk = gl.getExtension('EXT_color_buffer_float') || gl.getExtension('EXT_color_buffer_half_float');\n    if (floatOk) {\n      try {\n        sim = createSim(gl);\n      } catch (err) {\n        sim = null;\n      }\n    }\n  }\n\n  const still = () => Boolean(motionQuery && motionQuery.matches);\n  const scale = () => (SIZES[props.current.size] || SIZES.medium).k;\n  const podH = () => POD_H * scale();\n  const count = () => clamp(Math.round(num(props.current.jets, 2)), 1, MAX_JETS);\n  const offsets = () => {\n    const n = count();\n    if (n === 1) return [0];\n    const a = s.bw * (n === 2 ? 0.3 : 0.34);\n    return Array.from({ length: n }, (_, k) => a * ((2 * k) / (n - 1) - 1));\n  };\n  const feet = () => {\n    const xs = offsets();\n    return xs.length > 1 ? [xs[0], xs[xs.length - 1]] : [-0.3 * s.bw, 0.3 * s.bw];\n  };\n  const totalThrust = () => s.thrust[0] + s.thrust[1] + s.thrust[2] + s.thrust[3];\n  const flying = () =>\n    s.phase === 'ignite' || s.phase === 'climb' || s.phase === 'hover' || s.phase === 'descend' || s.phase === 'drag';\n  const riding = () => s.phase === 'drive' || s.phase === 'loop' || s.phase === 'flip' || s.phase === 'return';\n  const xLimits = () => [s.bw / 2 + 4, Math.max(s.bw / 2 + 4, s.W - s.bw / 2 - 4)];\n  const rampOn = () => Boolean(props.current.ramp);\n\n  const rampGeo = (): RampGeo => {\n    const base = podH() + s.bh / 2;\n    const key = `${s.W}|${s.H}|${s.bw}|${base}`;\n    if (s.rampKey === key && s.rampGeo) return s.rampGeo;\n    const r = Math.max(s.H * 0.24, podH() + s.bh + 40);\n    const K = 1 / r;\n    const ease = r * 0.4;\n    const arc = (r * Math.PI) / 2 - ease;\n    const build = (rise: number, top: number) => {\n      const segs = [\n        [ease, 0, K],\n        [arc, K, K],\n        [ease, K, 0],\n        [rise, 0, 0],\n        [ease, 0, K],\n        [arc, K, K],\n        [ease, K, 0],\n        [top, 0, 0]\n      ];\n      const g = { ss: [0], xs: [0], ys: [0], ps: [0], ks: [0] };\n      let x = 0;\n      let y = 0;\n      let a = 0;\n      let d = 0;\n      segs.forEach(([len, k0, k1]) => {\n        const n = Math.max(1, Math.round(len / 1.5));\n        const h = len / n;\n        for (let j = 0; j < n; j += 1) {\n          const km = k0 + (k1 - k0) * ((j + 0.5) / n);\n          const am = a + (km * h) / 2;\n          x += Math.cos(am) * h;\n          y += Math.sin(am) * h;\n          a += km * h;\n          d += h;\n          g.ss.push(d);\n          g.xs.push(x);\n          g.ys.push(y);\n          g.ps.push(a);\n          g.ks.push(k0 + (k1 - k0) * ((j + 1) / n));\n        }\n      });\n      return g;\n    };\n    const bends = build(0, 0);\n    const rise = Math.max(0, s.H * 0.66 - Math.max(...bends.ys));\n    const g = build(rise, Math.max(48, s.W * 0.16));\n    const T = clamp(r * 0.12, 8, 16);\n    const x0 = s.W - Math.max(18, s.W * 0.05) - T - Math.max(...g.xs);\n    const xs = g.xs.map(v => v + x0);\n    const n = g.ss.length - 1;\n    const footAt = g.ys.findIndex(v => v > 3);\n    const foot = footAt < 0 ? xs[n] : xs[footAt];\n    const geo = {\n      ...g,\n      xs,\n      r,\n      T,\n      x0,\n      foot,\n      S: g.ss[n],\n      clear: Math.min(foot, xs[n]) - 12,\n      ok: Math.max(...g.ys) + T < s.H - 8\n    };\n    s.rampKey = key;\n    s.rampGeo = geo;\n    return geo;\n  };\n\n  const home = () => {\n    const [lo, hi] = xLimits();\n    if (!rampOn()) return clamp(s.W / 2, lo, hi);\n    const g = rampGeo();\n    return clamp(Math.min(g.foot * 0.5, g.x0 - 90, g.clear - s.bw / 2 - 40), lo, hi);\n  };\n\n  const craftCenter = () => ({\n    x: s.x,\n    y: podH() + s.bh / 2 + s.y + REST\n  });\n\n  const nozzles = (): Nozzle[] => {\n    const c = craftCenter();\n    const cs = Math.cos(s.th);\n    const sn = Math.sin(s.th);\n    const ry = props.current.thrusters ? -(s.bh / 2 + podH()) + 1 : -(s.bh / 2) + 2;\n    return offsets().map(rx => ({ x: c.x + rx * cs - ry * sn, y: c.y + rx * sn + ry * cs, dx: sn, dy: -cs }));\n  };\n\n  const cssCenter = () => {\n    const c = craftCenter();\n    return { x: c.x, y: s.H - c.y };\n  };\n\n  const quad = (x0: number, w: number, h: number, p0: number[], p1: number[], p2: number[], p3: number[]) => {\n    const dx1 = p1[0] - p2[0];\n    const dx2 = p3[0] - p2[0];\n    const dx3 = p0[0] - p1[0] + p2[0] - p3[0];\n    const dy1 = p1[1] - p2[1];\n    const dy2 = p3[1] - p2[1];\n    const dy3 = p0[1] - p1[1] + p2[1] - p3[1];\n    const det = dx1 * dy2 - dx2 * dy1 || 1e-6;\n    const g = (dx3 * dy2 - dx2 * dy3) / det;\n    const k = (dx1 * dy3 - dx3 * dy1) / det;\n    const a = p1[0] - p0[0] + g * p1[0];\n    const b = p3[0] - p0[0] + k * p3[0];\n    const d = p1[1] - p0[1] + g * p1[1];\n    const e = p3[1] - p0[1] + k * p3[1];\n    const tx = p0[0] - (a * x0) / w;\n    const ty = p0[1] - (d * x0) / w;\n    if (Math.abs(g) < 1e-7 && Math.abs(k) < 1e-7) {\n      return `matrix(${[a / w, d / w, b / h, e / h, tx, ty].map(v => v.toFixed(6)).join(',')})`;\n    }\n    const m = [a / w, d / w, 0, g / w, b / h, e / h, 0, k / h, 0, 0, 1, 0, tx, ty, 0, 1 - (g * x0) / w];\n    return `matrix3d(${m.map(v => v.toFixed(6)).join(',')})`;\n  };\n\n  const dropStrip = () => {\n    if (!s.strip) return;\n    s.strip.wrap.remove();\n    s.strip.ink.remove();\n    s.strip = null;\n    button.style.visibility = '';\n  };\n\n  const makeStrip = () => {\n    dropStrip();\n    const t = s.track;\n    if (!t) return;\n    const g = t.geo;\n    const d = podH() + s.bh / 2;\n    const sn = [0];\n    for (let i = 1; i < g.ss.length; i += 1) {\n      const k = (g.ks[i] + g.ks[i - 1]) / 2;\n      sn.push(sn[i - 1] + (g.ss[i] - g.ss[i - 1]) * Math.max(1 - k * d, 0.2));\n    }\n    const n = clamp(Math.round(s.bw / 8), 8, 32);\n    const wrap = document.createElement('div');\n    wrap.setAttribute('aria-hidden', 'true');\n    wrap.style.cssText = 'position:absolute;inset:0;z-index:1;pointer-events:none';\n    const ink = document.createElement('div');\n    ink.setAttribute('aria-hidden', 'true');\n    ink.style.cssText = 'position:absolute;inset:0;z-index:2;pointer-events:none;will-change:transform';\n    const kids = Array.from(button.children) as HTMLElement[];\n    const label = kids[1];\n    const pods = kids.slice(2).map(p => {\n      const el = p.cloneNode(true) as HTMLElement;\n      el.style.left = '0px';\n      el.style.top = '0px';\n      el.style.transformOrigin = '0 0';\n      wrap.appendChild(el);\n      return { el, x: p.offsetLeft, y: p.offsetTop + p.offsetHeight / 2, k: 1 };\n    });\n    const parts: HTMLElement[] = [];\n    for (let i = 0; i < n; i += 1) {\n      const c = button.cloneNode(true) as HTMLButtonElement;\n      c.removeAttribute('data-hover');\n      c.removeAttribute('data-pressed');\n      c.setAttribute('tabindex', '-1');\n      c.setAttribute('inert', '');\n      Array.from(c.children)\n        .slice(1)\n        .forEach((k: Element) => {\n          (k as HTMLElement).style.visibility = 'hidden';\n        });\n      const x0 = (s.bw * i) / n - (i ? 0.75 : 2);\n      const x1 = (s.bw * (i + 1)) / n + (i < n - 1 ? 0.75 : 2);\n      c.style.visibility = 'visible';\n      c.style.transformOrigin = '0 0';\n      c.style.clipPath = `polygon(${x0}px -2px, ${x1}px -2px, ${x1}px ${s.bh + 2}px, ${x0}px ${s.bh + 2}px)`;\n      wrap.appendChild(c);\n      parts.push(c);\n    }\n    const items: Piece[] = [];\n    const text = label ? label.textContent || '' : '';\n    const ctx = document.createElement('canvas').getContext('2d');\n    if (label && ctx && text.trim() && !label.children.length) {\n      const cs = getComputedStyle(label);\n      const font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;\n      ctx.font = font;\n      const chars = Array.from(text);\n      const at = (i: number) => ctx.measureText(chars.slice(0, i).join('')).width;\n      const span = at(chars.length) || 1;\n      const yc = label.offsetTop + label.offsetHeight / 2;\n      chars.forEach((ch, i) => {\n        const el = document.createElement('span');\n        el.textContent = ch;\n        el.style.cssText = `position:absolute;left:0;top:0;white-space:pre;line-height:1;transform-origin:0 0;font:${font};color:${cs.color}`;\n        ink.appendChild(el);\n        items.push({ el, x: label.offsetLeft + ((at(i) + at(i + 1)) / 2) * (label.offsetWidth / span), y: yc, k: 0.5 });\n      });\n    } else if (label) {\n      const el = label.cloneNode(true) as HTMLElement;\n      el.style.position = 'absolute';\n      el.style.left = '0px';\n      el.style.top = '0px';\n      el.style.transformOrigin = '0 0';\n      ink.appendChild(el);\n      items.push({\n        el,\n        x: label.offsetLeft + label.offsetWidth / 2,\n        y: label.offsetTop + label.offsetHeight / 2,\n        k: 0\n      });\n    }\n    root.appendChild(wrap);\n    root.appendChild(ink);\n    button.style.visibility = 'hidden';\n    s.strip = { wrap, ink, parts, n, sn, pods, items };\n  };\n\n  const placeStrip = () => {\n    const st = s.strip;\n    const t = s.track;\n    if (!st || !t) return;\n    const g = t.geo;\n    const S = g.ss[g.ss.length - 1];\n    const E = st.sn[st.sn.length - 1];\n    const seek = (from: number[], to: number[], v: number) => {\n      let lo = 0;\n      let hi = from.length - 1;\n      while (hi - lo > 1) {\n        const mid = (lo + hi) >> 1;\n        if (from[mid] <= v) lo = mid;\n        else hi = mid;\n      }\n      return to[lo] + (to[hi] - to[lo]) * ((v - from[lo]) / (from[hi] - from[lo]));\n    };\n    const neutral = (u: number) => (u <= 0 ? u : u >= S ? E + u - S : seek(g.ss, st.sn, u));\n    const track = (v: number) => (v <= 0 ? v : v >= E ? S + v - E : seek(st.sn, g.ss, v));\n    const nc = neutral(s.u);\n    const lift = podH();\n    const map = (x: number, y: number) => {\n      const [px, py, a] = along(g, track(nc + x - s.bw / 2));\n      const h = lift + s.bh - y;\n      return [px - Math.sin(a) * h, s.H - py - Math.cos(a) * h, a];\n    };\n    const pose = (el: HTMLElement, x: number, y: number, a: number) => {\n      el.style.transform = `translate(${x.toFixed(2)}px, ${y.toFixed(2)}px) rotate(${(-a).toFixed(4)}rad) translate(-50%, -50%)`;\n    };\n    const w = s.bw / st.n;\n    const top: number[][] = [];\n    const low: number[][] = [];\n    for (let j = 0; j <= st.n; j += 1) {\n      top.push(map(j * w, 0));\n      low.push(map(j * w, s.bh));\n    }\n    st.parts.forEach((part, i) => {\n      part.style.transform = quad(i * w, w, s.bh, top[i], top[i + 1], low[i + 1], low[i]);\n    });\n    st.pods.forEach(p => {\n      const [x, y, a] = map(p.x, p.y);\n      pose(p.el, x, y, a);\n    });\n    st.items.forEach(it => {\n      const [bx, by, ba] = map(it.x, it.y);\n      const [ox, oy, oa] = map(s.bw / 2, it.y);\n      const o = it.x - s.bw / 2;\n      const rx = ox + Math.cos(oa) * o;\n      const ry = oy - Math.sin(oa) * o;\n      pose(it.el, rx + (bx - rx) * it.k, ry + (by - ry) * it.k, oa + (ba - oa) * it.k);\n    });\n  };\n\n  const place = () => {\n    if (s.strip) placeStrip();\n    const c = craftCenter();\n    const left = c.x - s.bw / 2;\n    const top = s.H - c.y - s.bh / 2;\n    button.style.transform = `translate3d(${left.toFixed(2)}px, ${top.toFixed(2)}px, 0) rotate(${(-s.th).toFixed(5)}rad)`;\n    root.style.setProperty('--fly-thrust', clamp(totalThrust() * 0.9, 0, 1.2).toFixed(3));\n  };\n\n  const drawRamp = () => {\n    if (!ramp) return;\n    const g = rampGeo();\n    const paths = ramp.querySelectorAll('path');\n    const body = paths[0];\n    const edge = paths[1];\n    const f = (v: number) => v.toFixed(1);\n    const line = (off: number) => {\n      const out: string[] = [];\n      for (let i = 0; i < g.ss.length; i += 2) {\n        const a = g.ps[i];\n        out.push(`${f(g.xs[i] + Math.sin(a) * off)} ${f(s.H - g.ys[i] + Math.cos(a) * off)}`);\n      }\n      const a = g.ps[g.ss.length - 1];\n      out.push(`${f(g.xs[g.ss.length - 1] + Math.sin(a) * off)} ${f(s.H - g.ys[g.ss.length - 1] + Math.cos(a) * off)}`);\n      return `M ${out.join(' L ')}`;\n    };\n    ramp.setAttribute('viewBox', `0 0 ${f(s.W)} ${f(s.H)}`);\n    ramp.style.visibility = g.ok ? '' : 'hidden';\n    if (body) {\n      body.setAttribute('d', line(g.T / 2));\n      body.setAttribute('stroke-width', f(g.T));\n    }\n    if (edge) edge.setAttribute('d', line(0.75));\n  };\n\n  const jetUniforms = (list: Nozzle[]): Jets => {\n    const a = new Float32Array(MAX_JETS * 4);\n    const b = new Float32Array(MAX_JETS * 4);\n    const bend = [0, 0, 0, 0];\n    const n = list.length;\n    const K = SMOKE;\n    const k = scale();\n    list.forEach((q, i) => {\n      const str = clamp(s.thrust[i] * n, 0, 2);\n      const reach = q.dy < -0.2 ? q.y / -q.dy : K.forceMax;\n      a.set([q.x, q.y, q.dx, q.dy], i * 4);\n      b.set(\n        [K.speed * Math.sqrt(str), K.radius * k, Math.min(K.forceMax, reach * K.reach), str > 0.004 ? str : 0],\n        i * 4\n      );\n      const across = s.fvx * -q.dy + s.fvy * q.dx;\n      bend[i] = clamp(-across * 0.22, -0.55 * reach, 0.55 * reach);\n    });\n    return { a, b, bend };\n  };\n\n  const draw = (dt: number) => {\n    if (!sim) return;\n    const P = props.current;\n    const list = nozzles();\n    const jets = jetUniforms(list);\n    const c = craftCenter();\n    const xs = offsets();\n    const k = scale();\n    const craft = [c.x, c.y, s.bw / 2, s.bh / 2];\n    const motion = [s.vx, s.vy, s.th, s.w];\n    const calm =\n      (1 - smooth((Math.abs(s.fvx) - 40) / 100)) * (1 - smooth((Math.abs(s.th) - 0.1) / 0.2)) * (riding() ? 0 : 1);\n    if (dt > 0) {\n      s.simTime += dt;\n      sim.step(dt, {\n        jets,\n        craft,\n        motion,\n        calm,\n        scale: k,\n        activity: s.activity,\n        jetX: [0, 1, 2, 3].map(i => (i < xs.length ? xs[i] : 0)),\n        jetN: xs.length,\n        spacing: xs.length > 1 ? xs[1] - xs[0] : 0,\n        wind: s.wind * SMOKE.wind,\n        time: s.simTime,\n        smoke: clamp(num(P.smoke, 1), 0, 3),\n        turbulence: clamp(num(P.turbulence, 1), 0, 3)\n      });\n    }\n    const smoke = rgb(P.smokeColor);\n    sim.render(canvas.width, canvas.height, {\n      jets,\n      craft,\n      motion,\n      scale: k,\n      activity: s.activity,\n      time: s.simTime,\n      light: P.theme === 'light',\n      smokeColor: smoke,\n      shadeColor: smoke.map((v, i) => v * 0.74 + [0.0, 0.012, 0.045][i]),\n      thrustColor: rgb(P.thrustColor)\n    });\n  };\n\n  const measure = () => {\n    const r = root.getBoundingClientRect();\n    const W0 = s.W;\n    s.W = Math.max(1, r.width);\n    s.H = Math.max(1, r.height);\n    s.bw = button.offsetWidth;\n    s.bh = button.offsetHeight;\n    if (!s.placed) {\n      s.x = home();\n      s.placed = true;\n    } else if (W0 > 0 && W0 !== s.W) s.x *= s.W / W0;\n    if (!riding()) {\n      const [lo, hi] = xLimits();\n      s.x = clamp(s.x, lo, hi);\n    }\n    s.dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const cw = Math.round(s.W * s.dpr);\n    const ch = Math.round(s.H * s.dpr);\n    if (canvas.width !== cw || canvas.height !== ch) {\n      canvas.width = cw;\n      canvas.height = ch;\n    }\n    if (sim) sim.resize(s.W, s.H);\n    drawRamp();\n    root.dataset.ready = '';\n    place();\n    if (!s.running) draw(0);\n  };\n\n  const overCraft = (px: number, py: number, margin = 0) => {\n    const c = cssCenter();\n    const a = s.th;\n    const dx = px - c.x;\n    const dy = py - c.y;\n    const lx = dx * Math.cos(a) - dy * Math.sin(a);\n    const ly = dx * Math.sin(a) + dy * Math.cos(a);\n    return Math.abs(lx) <= s.bw / 2 + 2 + margin && ly >= -s.bh / 2 - 2 - margin && ly <= s.bh / 2 + podH() + margin;\n  };\n\n  const begin = () => {\n    s.pt = 0;\n    s.armed = false;\n    s.seed = Math.random() * 100;\n    s.wind = Math.random() < 0.5 ? -1 - Math.random() : 1 + Math.random();\n    s.vrPrev = 0;\n    s.quiet = 0;\n    s.boost = false;\n  };\n\n  const launch = () => {\n    if (s.phase !== 'rest' || still()) return false;\n    const P = props.current;\n    begin();\n    s.phase = 'ignite';\n    s.delay = s.delay.map(() => Math.random() * 0.07);\n    const top = s.H - s.bh - podH() - 30 - REST;\n    s.alt = clamp(s.y + num(P.altitude, 170), s.y + 24, Math.max(s.y + 24, top));\n    s.climbT = 1.15 + (s.alt - s.y) / 420;\n    s.hold = Math.max(0, num(P.hoverTime, 2.4));\n    s.xRef = s.x;\n    wake();\n    return true;\n  };\n\n  const along = (g: RampGeo, u: number): number[] => {\n    if (u <= 0) return [g.x0 + u, 0, 0, 0];\n    let lo = 0;\n    let hi = g.ss.length - 1;\n    if (u >= g.ss[hi]) {\n      const a = g.ps[hi];\n      return [g.xs[hi] + Math.cos(a) * (u - g.ss[hi]), g.ys[hi] + Math.sin(a) * (u - g.ss[hi]), a, 0];\n    }\n    while (hi - lo > 1) {\n      const mid = (lo + hi) >> 1;\n      if (g.ss[mid] <= u) lo = mid;\n      else hi = mid;\n    }\n    const m = (u - g.ss[lo]) / (g.ss[hi] - g.ss[lo]);\n    return [g.xs, g.ys, g.ps, g.ks].map(v => v[lo] + (v[hi] - v[lo]) * m);\n  };\n\n  const ridePose = (t: Track, u: number) => {\n    const [x, y, a, k] = along(t.geo, u);\n    return { x: x - Math.sin(a) * t.base, cy: y + Math.cos(a) * t.base, th: a, bend: k * t.base };\n  };\n\n  const ride = () => {\n    if (s.phase !== 'rest' || still() || !rampOn()) return false;\n    const g = rampGeo();\n    const run = g.x0 - s.x;\n    if (run < 60 || !g.ok) return launch();\n    begin();\n    const base = podH() + s.bh / 2;\n    const top = Math.max(...g.ys) - 2 * base;\n    const v0 = Math.sqrt(2 * G_RIDE * top + 2.2 * G_RIDE * (g.r - base));\n    s.track = { geo: g, base, v0, acc: (v0 * v0) / (2 * run) };\n    s.phase = 'drive';\n    s.u = -run;\n    s.v = 0;\n    s.thrust = [0, 0, 0, 0];\n    makeStrip();\n    wake();\n    return true;\n  };\n\n  const chooseLanding = (lead: number) => {\n    const [lo, hi] = xLimits();\n    let x = s.x + lead;\n    if (rampOn()) x = Math.min(x, rampGeo().clear - s.bw / 2);\n    s.landX = clamp(x, lo, hi);\n  };\n\n  const physics = (dt: number) => {\n    const P = props.current;\n    const wob = clamp(num(P.wobble, 1), 0, 3);\n    s.pt += dt;\n    s.clock += dt;\n    if (s.phase === 'return') {\n      const b = s.back as Back;\n      const u = Math.min(s.pt / b.time, 1);\n      s.x = b.from + (b.to - b.from) * jerk(u);\n      s.vx = ((b.to - b.from) * jerkV(u)) / b.time;\n      s.y = -REST;\n      s.vy = 0;\n      s.th = 0;\n      s.w = 0;\n      if (u >= 1) {\n        s.phase = 'rest';\n        s.pt = 0;\n        s.vx = 0;\n      }\n      return;\n    }\n    if (s.phase === 'drive' || s.phase === 'loop') {\n      const t = s.track as Track;\n      const prev = ridePose(t, s.u);\n      if (s.phase === 'drive') s.v = Math.min(t.v0, s.v + t.acc * dt);\n      else s.v = Math.sqrt(Math.max(t.v0 * t.v0 - 2 * G_RIDE * (prev.cy - t.base), 0.2 * t.v0 * t.v0));\n      s.u += (s.v * dt) / Math.max(1 - prev.bend, 0.25);\n      if (s.phase === 'drive' && s.u >= 0) s.phase = 'loop';\n      const pose = ridePose(t, s.u);\n      s.vx = (pose.x - prev.x) / dt;\n      s.vy = (pose.cy - prev.cy) / dt;\n      s.w = (pose.th - prev.th) / dt;\n      s.x = pose.x;\n      s.y = pose.cy - podH() - s.bh / 2 - REST;\n      s.th = pose.th;\n      s.thrust = s.thrust.map(v => v * Math.exp(-dt / 0.1));\n      if (s.phase === 'loop' && s.u >= t.geo.S) {\n        dropStrip();\n        s.phase = 'flip';\n        s.pt = 0;\n        s.flip = { th: s.th, w: s.w };\n      }\n      return;\n    }\n    const I = (s.bw * s.bw + s.bh * s.bh) / 12;\n    const cs = Math.max(Math.cos(s.th), 0.5);\n    let total = 0;\n    let control = false;\n    let xt = s.xRef;\n    let kp = 3;\n    let kd = 3.6;\n    let turn: Turn | null = null;\n    if (s.phase === 'flip') {\n      const u = Math.min(s.pt / FLIP_TIME, 1);\n      const f = s.flip as Turn;\n      const end = Math.PI * 2;\n      turn = {\n        th:\n          (2 * u * u * u - 3 * u * u + 1) * f.th +\n          (u * u * u - 2 * u * u + u) * FLIP_TIME * f.w +\n          (3 * u * u - 2 * u * u * u) * end,\n        w: ((6 * u * u - 6 * u) * (f.th - end)) / FLIP_TIME + (3 * u * u - 4 * u + 1) * f.w\n      };\n      total = 2.4 * smooth((Math.cos(turn.th) - 0.8) / 0.15);\n      const nearGround = s.y + REST < s.bh * 0.6;\n      if (u >= 1 || nearGround) {\n        turn = null;\n        s.th -= Math.PI * 2 * Math.round(s.th / (Math.PI * 2));\n        s.phase = 'descend';\n        s.pt = 0;\n        s.boost = true;\n        s.vrPrev = s.vy;\n        chooseLanding((s.vx * Math.abs(s.vx)) / 700);\n      }\n    }\n    if (s.phase === 'ignite') {\n      total = 0.9 * smooth(s.pt / 0.5);\n      if (s.pt >= 0.5) {\n        s.phase = 'climb';\n        s.pt = 0;\n        s.y0 = s.y;\n      }\n    }\n    if (s.phase === 'climb') {\n      const u = s.pt / s.climbT;\n      const span = s.alt - s.y0;\n      const yr = s.y0 + span * jerk(u);\n      const vr = (span * jerkV(u)) / s.climbT;\n      const ar = (span * jerkA(u)) / (s.climbT * s.climbT);\n      total = (G + ar + 30 * (yr - s.y) + 8 * (vr - s.vy)) / (G * cs);\n      control = true;\n      if (u >= 1) {\n        s.phase = 'hover';\n        s.pt = 0;\n      }\n    } else if (s.phase === 'hover') {\n      const t = s.clock * 0.8;\n      const yr = s.alt + 3.5 * wob * wave(t, s.seed);\n      const vr = 3.5 * wob * 0.8 * waveD(t, s.seed);\n      total = (G + 30 * (yr - s.y) + 8 * (vr - s.vy)) / (G * cs);\n      control = true;\n      if (s.pt >= s.hold) {\n        s.phase = 'descend';\n        s.pt = 0;\n        s.vrPrev = 0;\n        chooseLanding(0);\n      }\n    } else if (s.phase === 'drag') {\n      const yr = s.target.y - podH() - s.bh / 2 - REST;\n      total = (G + 34 * (yr - s.y) - 10 * s.vy) / (G * cs);\n      control = true;\n    } else if (s.phase === 'descend') {\n      xt = s.landX;\n      kp = s.boost ? 5 : 4;\n      kd = s.boost ? 4.6 : 4.2;\n      const vmax = 170;\n      const vt = 32;\n      const off = s.boost ? smooth((Math.abs(s.landX - s.x) - 20) / 40) : smooth((Math.abs(s.landX - s.x) - 3) / 23);\n      const h = Math.max(s.y + REST, 0);\n      const profile = (vt + (vmax - vt) * (1 - Math.exp(-h / 52))) * (1 - (s.boost ? 0.45 : 0.88) * off);\n      const ramp = s.boost ? vmax * 3 : vmax * smooth(s.pt / 0.9) + 1;\n      const vr = -Math.min(profile, ramp);\n      const ar = clamp((vr - s.vrPrev) / Math.max(dt, 1e-4), -600, 600);\n      s.vrPrev = vr;\n      total = (G + ar + (s.boost ? 9 : 7) * (vr - s.vy)) / (G * cs);\n      control = true;\n      if (s.contact && s.pt > 0.3) {\n        s.phase = 'landed';\n        s.pt = 0;\n        s.boost = false;\n      }\n    } else if (s.phase === 'landed') {\n      total = 0;\n      if (s.pt > 0.6 && totalThrust() < 0.03) {\n        const back = rampOn() && s.track ? home() : s.x;\n        s.phase = Math.abs(back - s.x) > 6 ? 'return' : 'rest';\n        s.pt = 0;\n        s.thrust = [0, 0, 0, 0];\n        s.vy = 0;\n        s.w = 0;\n        s.vx = 0;\n        s.track = null;\n        s.back = { from: s.x, to: back, time: clamp(Math.abs(back - s.x) / 200, 0.7, 1.8) };\n      }\n    }\n    total = Number.isFinite(total) ? clamp(total, 0, s.boost || s.phase === 'flip' ? 2.6 : 1.9) : 1;\n\n    const xs = offsets();\n    const n = xs.length;\n    const sx2 = xs.reduce((acc, v) => acc + v * v, 0);\n    const hand = s.phase === 'drag' ? clamp(100 * (s.target.x - s.x) - 17 * s.vx, -2400, 2400) : 0;\n    let alphaCmd = 0;\n    if (control) {\n      const gust = 0.8 * wob * wave(s.clock * 1.7, s.seed + 3);\n      const aCmd = s.phase === 'drag' ? hand * 0.75 : kp * (xt - s.x) - kd * s.vx;\n      const lean = s.boost ? 0.45 : 0.32;\n      const thRef = clamp(-Math.atan2(aCmd, G), -lean, lean);\n      alphaCmd = -81 * (s.th - thRef) - 14 * s.w - gust;\n    }\n    const cap = (s.boost || s.phase === 'flip' ? 2.6 : 2.4) / n;\n    const spool = (cur: number, cmd: number) => cur + (cmd - cur) * (1 - Math.exp(-dt / (cmd > cur ? 0.08 : 0.17)));\n    const unit = sx2 > 1 ? (alphaCmd * I) / (G * sx2) : 0;\n    const room = Math.abs(unit) * Math.max(0, ...xs.map(Math.abs));\n    const base = Math.min(total / n, Math.max(cap - room, cap * 0.5));\n    let torque = 0;\n    for (let k = 0; k < MAX_JETS; k += 1) {\n      let cmd = 0;\n      if (k < n) {\n        cmd = clamp(base + unit * xs[k], 0, cap);\n        if (s.phase === 'ignite' && s.pt < s.delay[k]) cmd = 0;\n      }\n      s.thrust[k] = spool(s.thrust[k], cmd);\n      if (k < n) torque += s.thrust[k] * xs[k];\n    }\n\n    const T = totalThrust() * G;\n    let ax = s.phase === 'drag' ? hand : -T * Math.sin(s.th) - 0.8 * s.vx;\n    let ay = T * Math.cos(s.th) - G - 0.3 * s.vy;\n    let alpha = s.phase === 'flip' ? 0 : (torque * G) / I - 1.2 * s.w;\n    if (control) {\n      if (s.phase !== 'drag') ax += 12 * wob * wave(s.clock * 0.7, s.seed + 7);\n      alpha += 0.9 * wob * wave(s.clock * 2.3, s.seed + 11);\n      if (sx2 < 1) alpha += alphaCmd;\n      if (Math.abs(s.th) > 0.35) alpha -= (s.th - Math.sign(s.th) * 0.35) * 160;\n    }\n    const c = craftCenter();\n    const csn = Math.cos(s.th);\n    const sn = Math.sin(s.th);\n    const ry = -(s.bh / 2 + podH());\n    let contact = false;\n    if (s.phase !== 'flip') {\n      feet().forEach(rx => {\n        const ox = rx * csn - ry * sn;\n        const oy = rx * sn + ry * csn;\n        const footY = c.y + oy;\n        const pen = REST - footY;\n        if (pen > 0) {\n          contact = true;\n          const vpy = s.vy + s.w * ox;\n          const vpx = s.vx - s.w * oy;\n          const fy = Math.max(0, SPRING * pen - DAMP * vpy);\n          const fx = -10 * vpx;\n          ay += fy;\n          ax += fx;\n          alpha += (ox * fy - oy * fx) / I;\n        }\n      });\n    }\n    s.contact = contact;\n    s.vx = clamp(s.vx + ax * dt, -1500, 1500);\n    s.vy = clamp(s.vy + ay * dt, -1500, 1500);\n    s.w = clamp(s.w + alpha * dt, -14, 14);\n    s.x += s.vx * dt;\n    s.y += s.vy * dt;\n    s.th += s.w * dt;\n    if (turn) {\n      s.th = turn.th;\n      s.w = turn.w;\n    }\n    if (s.phase !== 'flip' && Math.abs(s.th) > TILT_MAX) {\n      s.th = Math.sign(s.th) * TILT_MAX;\n      if (s.w * s.th > 0) s.w = 0;\n    }\n    const [lo, hi] = xLimits();\n    if (s.x < lo || s.x > hi) {\n      s.x = clamp(s.x, lo, hi);\n      s.vx = 0;\n    }\n    if (s.y + REST < -40) {\n      s.y = -REST;\n      s.vy = 0;\n    }\n    if (!Number.isFinite(s.y + s.x + s.th + s.vy + s.vx + s.w + totalThrust())) {\n      Object.assign(s, { x: s.W / 2, vx: 0, y: -REST, vy: 0, th: 0, w: 0, thrust: [0, 0, 0, 0], phase: 'rest' });\n      s.drag = null;\n    }\n  };\n\n  const updatePointer = (dt: number) => {\n    const P = props.current;\n    const p = s.pointer;\n    const c = cssCenter();\n    if (s.attached && !s.drag && s.lastC) {\n      const fx = p.x + s.off.x;\n      const fy = p.y + s.off.y;\n      const da = -(s.th - s.lastTh);\n      const rx = fx - s.lastC.x;\n      const ry = fy - s.lastC.y;\n      const nx = c.x + rx * Math.cos(da) - ry * Math.sin(da);\n      const ny = c.y + rx * Math.sin(da) + ry * Math.cos(da);\n      s.off.x += nx - fx;\n      s.off.y += ny - fy;\n    }\n    s.lastC = c;\n    s.lastTh = s.th;\n    const fx = p.x + s.off.x;\n    const fy = p.y + s.off.y;\n    const over = p.in && overCraft(fx, fy, s.attached || s.drag ? 10 : 0);\n    if (over !== s.hover) {\n      s.hover = over;\n      if (over) button.dataset.hover = '';\n      else delete button.dataset.hover;\n    }\n    if (!over && !s.drag) {\n      s.attached = false;\n      if (s.phase === 'rest') s.armed = true;\n    }\n    if (over && s.armed && s.phase === 'rest' && p.type === 'mouse' && P.trigger !== 'click' && !rampOn() && launch()) {\n      s.attached = Boolean(P.carryCursor);\n    }\n    if (s.phase === 'rest' && Math.abs(s.off.x) + Math.abs(s.off.y) < 12) {\n      const k = Math.exp(-dt * 6);\n      s.off.x *= k;\n      s.off.y *= k;\n    }\n    if (Math.abs(s.off.x) + Math.abs(s.off.y) < 0.3 && (!s.attached || s.phase === 'rest')) {\n      s.off.x = 0;\n      s.off.y = 0;\n      if (s.phase === 'rest') s.attached = false;\n    }\n    const carry =\n      Boolean(P.carryCursor) &&\n      p.in &&\n      p.type === 'mouse' &&\n      (s.attached || Boolean(s.drag) || s.off.x !== 0 || s.off.y !== 0);\n    if (carry !== s.carry) {\n      s.carry = carry;\n      if (carry) root.dataset.carry = '';\n      else delete root.dataset.carry;\n    }\n    if (carry && cursor) {\n      cursor.style.transform = `translate3d(${(fx - 1.8).toFixed(2)}px, ${(fy - 1.75).toFixed(2)}px, 0)`;\n    }\n  };\n\n  const frame = (now: number) => {\n    s.raf = 0;\n    const dt = s.last ? clamp((now - s.last) / 1000, 0, 1 / 30) : 1 / 60;\n    s.last = now;\n    if (dt > 0) {\n      if (s.phase !== 'rest') {\n        const steps = 6;\n        for (let i = 0; i < steps; i += 1) physics(dt / steps);\n      }\n      const kf = 1 - Math.exp(-dt / 0.12);\n      s.fvx += (s.vx - s.fvx) * kf;\n      s.fvy += (s.vy - s.fvy) * kf;\n      updatePointer(dt);\n      place();\n      draw(dt);\n      const thrust = totalThrust();\n      s.quiet = thrust > 0.01 || s.phase !== 'rest' ? 0 : s.quiet + dt;\n      const firing = thrust > 0.05 ? 1 : 0;\n      s.activity += (firing - s.activity) * (1 - Math.exp(-dt / (firing ? 0.25 : 1.6)));\n    }\n    if (s.phase !== 'rest' || s.quiet < 7 || s.carry) s.raf = requestAnimationFrame(frame);\n    else {\n      s.running = false;\n      s.last = 0;\n      if (sim) sim.clear();\n      draw(0);\n    }\n  };\n\n  const wake = () => {\n    if (s.raf) return;\n    s.running = true;\n    s.raf = requestAnimationFrame(frame);\n  };\n\n  const local = (e: PointerEvent) => {\n    const r = root.getBoundingClientRect();\n    return { x: e.clientX - r.left, y: e.clientY - r.top };\n  };\n\n  const pointerAt = () => {\n    const p = s.pointer;\n    const mouse = p.type === 'mouse';\n    return { x: p.x + (mouse ? s.off.x : 0), y: p.y + (mouse ? s.off.y : 0) };\n  };\n\n  const aim = () => {\n    if (!s.drag) return;\n    const f = pointerAt();\n    const [lo, hi] = xLimits();\n    const low = podH() + s.bh / 2;\n    s.target.x = clamp(f.x - s.drag.gx, lo, hi);\n    s.target.y = clamp(s.H - (f.y - s.drag.gy), low, Math.max(low, s.H - s.bh / 2 - 6));\n  };\n\n  const startDrag = (e: PointerEvent) => {\n    const f = pointerAt();\n    const c = cssCenter();\n    s.drag = { gx: f.x - c.x, gy: f.y - c.y };\n    s.phase = 'drag';\n    s.pt = 0;\n    s.attached = false;\n    s.pressed = false;\n    s.suppress = true;\n    s.boost = false;\n    delete button.dataset.pressed;\n    root.dataset.dragging = '';\n    try {\n      root.setPointerCapture(e.pointerId);\n    } catch {}\n    aim();\n  };\n\n  const endDrag = (e: PointerEvent) => {\n    s.drag = null;\n    s.press = null;\n    delete root.dataset.dragging;\n    try {\n      root.releasePointerCapture(e.pointerId);\n    } catch {}\n    s.phase = 'hover';\n    s.pt = 0;\n    s.alt = s.y;\n    const [lo, hi] = xLimits();\n    s.xRef = clamp(s.x + s.vx * 0.3, lo, hi);\n    s.hold = Math.min(Math.max(0, num(props.current.hoverTime, 2.4)), 0.9);\n  };\n\n  const onMove = (e: PointerEvent) => {\n    const q = local(e);\n    const p = s.pointer;\n    if (p.in && !s.attached && !s.drag && (s.off.x || s.off.y)) {\n      const k = Math.exp(-Math.hypot(q.x - p.x, q.y - p.y) / 110);\n      s.off.x *= k;\n      s.off.y *= k;\n    }\n    p.x = q.x;\n    p.y = q.y;\n    p.in = true;\n    p.type = e.pointerType || 'mouse';\n    if (s.press && !s.drag && flying() && Math.hypot(q.x - s.press.x, q.y - s.press.y) > 4) startDrag(e);\n    if (s.drag) aim();\n    updatePointer(0);\n    if (s.carry || s.phase !== 'rest') wake();\n  };\n\n  const onLeave = () => {\n    if (s.drag) return;\n    s.pointer.in = false;\n    s.attached = false;\n    s.off.x = 0;\n    s.off.y = 0;\n    updatePointer(0);\n  };\n\n  const onDown = (e: PointerEvent) => {\n    const q = local(e);\n    const p = s.pointer;\n    p.x = q.x;\n    p.y = q.y;\n    p.in = true;\n    p.type = e.pointerType || 'mouse';\n    s.suppress = false;\n    const f = pointerAt();\n    if (!overCraft(f.x, f.y, s.attached ? 10 : 0)) return;\n    if (p.type !== 'mouse' && s.phase === 'rest' && props.current.trigger !== 'click' && !rampOn()) launch();\n    s.press = { x: q.x, y: q.y };\n    if (s.carry && p.type === 'mouse') {\n      s.pressed = true;\n      button.dataset.pressed = '';\n    }\n  };\n\n  const onUp = (e: PointerEvent) => {\n    if (s.drag) {\n      endDrag(e);\n      return;\n    }\n    s.press = null;\n    if (!s.pressed) return;\n    s.pressed = false;\n    delete button.dataset.pressed;\n    if (s.carry && s.hover) {\n      if (s.phase === 'rest' && rampOn()) ride();\n      else if (props.current.trigger === 'click' && launch()) s.attached = Boolean(props.current.carryCursor);\n      if (props.current.onClick) props.current.onClick(e);\n    }\n  };\n\n  const onClick = (e: MouseEvent) => {\n    if (s.suppress) {\n      s.suppress = false;\n      e.stopPropagation();\n      e.preventDefault();\n      return;\n    }\n    if (s.phase !== 'rest') return;\n    if (rampOn()) {\n      ride();\n      return;\n    }\n    if (launch() && s.pointer.type === 'mouse' && s.pointer.in && s.hover) {\n      s.attached = Boolean(props.current.carryCursor);\n    }\n  };\n\n  root.addEventListener('pointermove', onMove);\n  root.addEventListener('pointerenter', onMove);\n  root.addEventListener('pointerleave', onLeave);\n  root.addEventListener('pointerdown', onDown);\n  root.addEventListener('pointerup', onUp);\n  root.addEventListener('pointercancel', onUp);\n  button.addEventListener('click', onClick);\n\n  const ro = new ResizeObserver(measure);\n  ro.observe(root);\n  ro.observe(button);\n  measure();\n\n  return {\n    refresh: measure,\n    rehome() {\n      measure();\n      if (s.phase !== 'rest') return;\n      s.x = home();\n      place();\n      if (!s.running) draw(0);\n    },\n    destroy() {\n      ro.disconnect();\n      dropStrip();\n      if (s.raf) cancelAnimationFrame(s.raf);\n      root.removeEventListener('pointermove', onMove);\n      root.removeEventListener('pointerenter', onMove);\n      root.removeEventListener('pointerleave', onLeave);\n      root.removeEventListener('pointerdown', onDown);\n      root.removeEventListener('pointerup', onUp);\n      root.removeEventListener('pointercancel', onUp);\n      button.removeEventListener('click', onClick);\n      if (sim) sim.dispose();\n    }\n  };\n}\n\nfunction createSim(gl: WebGL2RenderingContext) {\n  const compile = (type: number, src: string) => {\n    const sh = gl.createShader(type) as WebGLShader;\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {\n      const log = gl.getShaderInfoLog(sh);\n      gl.deleteShader(sh);\n      throw new Error(log || 'shader');\n    }\n    return sh;\n  };\n  const vs = compile(gl.VERTEX_SHADER, VERT);\n  const programs: Program[] = [];\n  const make = (src: string): Program => {\n    const fs = compile(gl.FRAGMENT_SHADER, src);\n    const p = gl.createProgram() as WebGLProgram;\n    gl.attachShader(p, vs);\n    gl.attachShader(p, fs);\n    gl.linkProgram(p);\n    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || 'program');\n    gl.deleteShader(fs);\n    const u: Record<string, WebGLUniformLocation | null> = {};\n    const count = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS);\n    for (let i = 0; i < count; i += 1) {\n      const info = gl.getActiveUniform(p, i) as WebGLActiveInfo;\n      u[info.name.replace(/\\[0\\]$/, '')] = gl.getUniformLocation(p, info.name);\n    }\n    const prog = { p, u };\n    programs.push(prog);\n    return prog;\n  };\n\n  const P = {\n    advect: make(ADVECT),\n    force: make(FORCE),\n    curl: make(CURL),\n    vorticity: make(VORTICITY),\n    divergence: make(DIVERGENCE),\n    pressure: make(PRESSURE),\n    gradient: make(GRADIENT),\n    scale: make(SCALE),\n    blur: make(BLUR),\n    dye: make(DYE),\n    render: make(RENDER)\n  };\n\n  const vao = gl.createVertexArray();\n  const targets: Target[] = [];\n\n  const target = (w: number, h: number, internal: number, mips: boolean): Target => {\n    const tex = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, tex);\n    const levels = mips ? Math.floor(Math.log2(Math.max(w, h))) + 1 : 1;\n    gl.texStorage2D(gl.TEXTURE_2D, levels, internal, w, h);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mips ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    const fbo = gl.createFramebuffer();\n    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);\n    gl.viewport(0, 0, w, h);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    const t = { tex, fbo, w, h, mips };\n    targets.push(t);\n    return t;\n  };\n  const pair = (w: number, h: number, internal: number, mips: boolean): Pair => {\n    const p = { read: target(w, h, internal, mips), write: target(w, h, internal, mips) } as Pair;\n    p.swap = () => {\n      const t = p.read;\n      p.read = p.write;\n      p.write = t;\n    };\n    return p;\n  };\n\n  let f: {\n    vw: number;\n    vh: number;\n    dw: number;\n    dh: number;\n    velocity: Pair;\n    pressure: Pair;\n    divergence: Target;\n    curl: Target;\n    dye: Pair;\n    soft: Target;\n    temp: Target;\n  } | null = null;\n  let world = [1, 1];\n\n  const free = () => {\n    targets.forEach(t => {\n      gl.deleteTexture(t.tex);\n      gl.deleteFramebuffer(t.fbo);\n    });\n    targets.length = 0;\n  };\n\n  const resize = (W: number, H: number) => {\n    const cell = Math.max(2, W / 520, H / 300);\n    const vw = Math.max(16, Math.round(W / cell));\n    const vh = Math.max(16, Math.round(H / cell));\n    const dw = Math.max(16, Math.round(W / (cell / 2)));\n    const dh = Math.max(16, Math.round(H / (cell / 2)));\n    world = [W, H];\n    if (f && f.vw === vw && f.vh === vh && f.dw === dw && f.dh === dh) return;\n    free();\n    f = {\n      vw,\n      vh,\n      dw,\n      dh,\n      velocity: pair(vw, vh, gl.RG16F, false),\n      pressure: pair(vw, vh, gl.R16F, false),\n      divergence: target(vw, vh, gl.R16F, false),\n      curl: target(vw, vh, gl.R16F, false),\n      dye: pair(dw, dh, gl.RG16F, false),\n      soft: target(dw, dh, gl.RG16F, true),\n      temp: target(dw, dh, gl.RG16F, false)\n    };\n  };\n\n  let unit = 0;\n  const bind = (prog: Program, values: Record<string, Uniform>) => {\n    gl.useProgram(prog.p);\n    unit = 0;\n    Object.entries(values).forEach(([name, v]) => {\n      const loc = prog.u[name];\n      if (loc === undefined || loc === null) return;\n      if (typeof v === 'number') gl.uniform1f(loc, v);\n      else if ('f1' in v) gl.uniform1fv(loc, v.f1);\n      else if ('tex' in v) {\n        gl.activeTexture(gl.TEXTURE0 + unit);\n        gl.bindTexture(gl.TEXTURE_2D, v.tex);\n        gl.uniform1i(loc, unit);\n        unit += 1;\n      } else if (v.length === 2) gl.uniform2fv(loc, v);\n      else if (v.length === 3) gl.uniform3fv(loc, v);\n      else gl.uniform4fv(loc, v);\n    });\n  };\n  const run = (prog: Program, values: Record<string, Uniform>, out?: Target) => {\n    bind(prog, values);\n    if (out) {\n      gl.bindFramebuffer(gl.FRAMEBUFFER, out.fbo);\n      gl.viewport(0, 0, out.w, out.h);\n    }\n    gl.bindVertexArray(vao);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  };\n\n  const step = (dt: number, o: StepInput) => {\n    if (!f) return;\n    const K = SMOKE;\n    gl.disable(gl.BLEND);\n    const vt = [1 / f.vw, 1 / f.vh];\n    run(\n      P.advect,\n      { uVelocity: f.velocity.read, uSource: f.velocity.read, uWorld: world, uDt: dt, uKeep: 1 },\n      f.velocity.write\n    );\n    f.velocity.swap();\n    run(\n      P.force,\n      {\n        uVelocity: f.velocity.read,\n        uDye: f.soft,\n        uWorld: world,\n        uDt: dt,\n        uBuoyancy: K.buoy,\n        uLift: K.lift,\n        uDamping: K.damp,\n        uSpread: K.spread,\n        uSplash: K.splash,\n        uFriction: K.friction,\n        uWind: o.wind,\n        uTurb: [K.turb * (0.35 + 0.65 * o.activity), K.turbScale],\n        uTime: o.time,\n        uObstacle: o.calm,\n        uKick: [K.kickNear * o.scale, K.kickW * o.scale, K.kickH * o.scale],\n        uCraft: o.craft,\n        uCraftMotion: o.motion,\n        uJetA: o.jets.a,\n        uJetB: o.jets.b,\n        uJetBend: { f1: o.jets.bend }\n      },\n      f.velocity.write\n    );\n    f.velocity.swap();\n    run(P.curl, { uVelocity: f.velocity.read, uTexel: vt }, f.curl);\n    run(\n      P.vorticity,\n      {\n        uVelocity: f.velocity.read,\n        uCurl: f.curl,\n        uTexel: vt,\n        uStrength: K.vort * o.turbulence * (0.4 + 0.6 * o.activity),\n        uDt: dt\n      },\n      f.velocity.write\n    );\n    f.velocity.swap();\n    run(P.divergence, { uVelocity: f.velocity.read, uTexel: vt }, f.divergence);\n    run(P.scale, { uSource: f.pressure.read, uScale: 0.8 }, f.pressure.write);\n    f.pressure.swap();\n    for (let i = 0; i < K.iters; i += 1) {\n      run(P.pressure, { uPressure: f.pressure.read, uDivergence: f.divergence, uTexel: vt }, f.pressure.write);\n      f.pressure.swap();\n    }\n    run(P.gradient, { uPressure: f.pressure.read, uVelocity: f.velocity.read, uTexel: vt }, f.velocity.write);\n    f.velocity.swap();\n\n    run(\n      P.dye,\n      {\n        uVelocity: f.velocity.read,\n        uSource: f.dye.read,\n        uWorld: world,\n        uDt: dt,\n        uKeep: [Math.exp(-(K.keepD + K.keepIdle * (1 - o.activity)) * dt), Math.exp(-K.keepH * dt)],\n        uRate: [K.rateN * o.smoke, K.rateH, K.rateK * o.smoke, K.rateC * o.smoke],\n        uSpread: K.spread,\n        uCol: [K.colSpread, K.colR * o.scale, K.colD, K.colU],\n        uKick: [K.kickNear * o.scale, K.kickW * o.scale, K.kickH * o.scale],\n        uDisperse: K.disperse,\n        uDispV: K.dispV,\n        uCraft: o.craft,\n        uCraftMotion: o.motion,\n        uGap: [o.spacing, K.gap * o.calm],\n        uJetX: { f1: o.jetX },\n        uJetN: o.jetN,\n        uFloor: K.floor,\n        uTime: o.time,\n        uJetA: o.jets.a,\n        uJetB: o.jets.b,\n        uJetBend: { f1: o.jets.bend }\n      },\n      f.dye.write\n    );\n    f.dye.swap();\n    const spread = K.blur / 2;\n    run(P.blur, { uSource: f.dye.read, uStep: [spread / f.dw, 0] }, f.temp);\n    run(P.blur, { uSource: f.temp, uStep: [0, spread / f.dh] }, f.soft);\n    gl.bindTexture(gl.TEXTURE_2D, f.soft.tex);\n    gl.generateMipmap(gl.TEXTURE_2D);\n  };\n\n  const render = (cw: number, ch: number, o: RenderInput) => {\n    if (!f) return;\n    const K = SMOKE;\n    gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n    gl.viewport(0, 0, cw, ch);\n    gl.disable(gl.BLEND);\n    run(P.render, {\n      uDye: f.soft,\n      uWorld: world,\n      uDyeSize: [f.dw, f.dh],\n      uTime: o.time,\n      uLightTheme: o.light ? 1 : 0,\n      uSmoke: o.smokeColor,\n      uShade: o.shadeColor,\n      uThrust: o.thrustColor,\n      uLook: [K.alphaK * (0.6 + 0.4 * o.activity), K.relief, K.shadow, K.alphaLod],\n      uLook2: [K.ambient, K.key, K.wrap, K.ao],\n      uLook3: [K.normalE, K.normalLod, K.fine, K.broad],\n      uCol: [K.colSpread, K.colR * o.scale, K.colD, K.colU],\n      uScale: o.scale,\n      uGamma: K.gamma,\n      uTone: K.tone,\n      uLight2: [K.jetLight, K.emit, K.exposure],\n      uThreshold: K.threshold * (0.3 + 0.7 * o.activity),\n      uCraft: o.craft,\n      uCraftMotion: o.motion,\n      uJetA: o.jets.a,\n      uJetB: o.jets.b,\n      uJetBend: { f1: o.jets.bend }\n    });\n  };\n\n  const clear = () => {\n    if (!f) return;\n    [\n      f.velocity.read,\n      f.velocity.write,\n      f.pressure.read,\n      f.pressure.write,\n      f.dye.read,\n      f.dye.write,\n      f.soft,\n      f.temp\n    ].forEach(t => {\n      gl.bindFramebuffer(gl.FRAMEBUFFER, t.fbo);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n    });\n    gl.bindTexture(gl.TEXTURE_2D, f.soft.tex);\n    gl.generateMipmap(gl.TEXTURE_2D);\n  };\n\n  const dispose = () => {\n    free();\n    programs.forEach(p => gl.deleteProgram(p.p));\n    gl.deleteShader(vs);\n    gl.deleteVertexArray(vao);\n  };\n\n  return { step, render, resize, clear, dispose };\n}\n\nexport interface FlyingButtonProps {\n  children?: ReactNode;\n  onClick?: ClickHandler;\n  theme?: Theme;\n  size?: Size;\n  radius?: number;\n  altitude?: number;\n  hoverTime?: number;\n  jets?: number;\n  thrusters?: boolean;\n  trigger?: Trigger;\n  carryCursor?: boolean;\n  ramp?: boolean;\n  smoke?: number;\n  turbulence?: number;\n  wobble?: number;\n  thrustColor?: string;\n  smokeColor?: string;\n  buttonColor?: string;\n  textColor?: string;\n  rampColor?: string;\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport default function FlyingButton({\n  children = 'Take off',\n  onClick,\n  theme = 'dark',\n  size = 'medium',\n  radius,\n  altitude = 170,\n  hoverTime = 2.4,\n  jets = 2,\n  thrusters = true,\n  trigger = 'hover',\n  carryCursor = true,\n  ramp = false,\n  rampColor,\n  smoke = 1,\n  turbulence = 1,\n  wobble = 1,\n  thrustColor,\n  smokeColor,\n  buttonColor,\n  textColor,\n  className = '',\n  style\n}: FlyingButtonProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const cursorRef = useRef<SVGSVGElement>(null);\n  const rampRef = useRef<SVGSVGElement>(null);\n  const engineRef = useRef<Engine | null>(null);\n  const propsRef = useRef<EngineProps>({} as EngineProps);\n  const light = theme === 'light';\n  const dims = SIZES[size as Size] || SIZES.medium;\n  const thrust = thrustColor || (light ? '#4f8cff' : '#8ec5ff');\n  const smokeTone = smokeColor || (light ? '#e9ebf0' : '#eef1f6');\n  const body = buttonColor || (light ? '#18181b' : '#27272a');\n  const ink = textColor || (light ? '#fafafa' : '#f5f5f5');\n  const deck = rampColor || (light ? '#ececf0' : '#25222b');\n  const jetCount = clamp(Math.round(num(jets, 2)), 1, MAX_JETS);\n  const spread = jetCount === 1 ? 0 : jetCount === 2 ? 0.3 : 0.34;\n  const pods = thrusters\n    ? Array.from({ length: jetCount }, (_, k) =>\n        jetCount === 1 ? 50 : 50 + spread * 100 * ((2 * k) / (jetCount - 1) - 1)\n      )\n    : [];\n  propsRef.current = {\n    size,\n    altitude,\n    hoverTime,\n    jets: jetCount,\n    thrusters: Boolean(thrusters),\n    trigger,\n    carryCursor,\n    ramp: Boolean(ramp),\n    smoke,\n    turbulence,\n    wobble,\n    thrustColor: thrust,\n    smokeColor: smokeTone,\n    theme,\n    onClick\n  };\n\n  useEffect(() => {\n    if (!rootRef.current || !canvasRef.current || !buttonRef.current) return;\n    const engine = createEngine({\n      root: rootRef.current,\n      canvas: canvasRef.current,\n      button: buttonRef.current,\n      cursor: cursorRef.current,\n      ramp: rampRef.current,\n      props: propsRef\n    });\n    engineRef.current = engine;\n    return () => {\n      engineRef.current = null;\n      engine.destroy();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (engineRef.current) engineRef.current.refresh();\n  }, [size, jetCount, thrusters, radius]);\n\n  useEffect(() => {\n    if (engineRef.current) engineRef.current.rehome();\n  }, [ramp]);\n\n  const face = light\n    ? {\n        background:\n          'linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0) 50%), linear-gradient(0deg, color-mix(in srgb, var(--fly-glow) calc(var(--fly-thrust) * 30%), transparent), transparent 62%)',\n        boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.06), inset 0 -1px 0 rgba(0, 0, 0, 0.08)'\n      }\n    : {\n        background:\n          'linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0) 46%), linear-gradient(0deg, color-mix(in srgb, var(--fly-glow) calc(var(--fly-thrust) * 34%), transparent), transparent 62%)',\n        boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 -1px 0 rgba(0, 0, 0, 0.25)'\n      };\n  const hoverTone = light ? 'data-[hover]:brightness-[0.97]' : 'data-[hover]:brightness-[1.12]';\n  const k = dims.k;\n  const edge = light\n    ? 'color-mix(in srgb, var(--fly-ramp) 82%, #000)'\n    : 'color-mix(in srgb, var(--fly-ramp) 72%, #fff)';\n\n  return (\n    <div\n      ref={rootRef}\n      className={`group/fly relative isolate h-full min-h-[240px] w-full touch-manipulation overflow-hidden data-[carry]:cursor-none [&[data-dragging]:not([data-carry])]:cursor-grabbing ${className}`.trim()}\n      data-theme={theme}\n      data-trigger={ramp ? 'click' : trigger}\n      style={\n        {\n          '--fly-bg': body,\n          '--fly-ink': ink,\n          '--fly-glow': thrust,\n          '--fly-ramp': deck,\n          '--fly-pod': '#18181b',\n          '--fly-thrust': 0,\n          ...style\n        } as CSSProperties\n      }\n    >\n      <svg\n        ref={rampRef}\n        className=\"pointer-events-none absolute inset-0 block h-full w-full fill-none\"\n        style={ramp ? undefined : { display: 'none' }}\n        aria-hidden=\"true\"\n      >\n        <path style={{ stroke: 'var(--fly-ramp)', strokeLinejoin: 'round' }} />\n        <path style={{ stroke: edge, strokeWidth: 1.5, strokeLinejoin: 'round' }} />\n      </svg>\n      <canvas ref={canvasRef} className=\"pointer-events-none absolute inset-0 block h-full w-full\" aria-hidden=\"true\" />\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        className={`group/craft invisible absolute top-0 left-0 z-[1] m-0 inline-flex cursor-pointer touch-none items-center justify-center border-0 leading-none font-medium tracking-[0.01em] whitespace-nowrap outline-none select-none [-webkit-tap-highlight-color:transparent] [background:var(--fly-bg)] [color:var(--fly-ink)] [font-family:inherit] [transform-origin:50%_50%] [will-change:transform] focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--fly-ink)_60%,transparent)] focus-visible:[outline-offset:3px] group-data-[ready]/fly:visible group-data-[trigger=hover]/fly:cursor-default group-data-[carry]/fly:pointer-events-none group-data-[carry]/fly:cursor-none ${hoverTone}`}\n        style={{\n          height: dims.h,\n          padding: `0 ${dims.px}px`,\n          fontSize: dims.fs,\n          borderRadius: radius !== undefined && radius !== null ? num(radius, 0) : dims.h / 2\n        }}\n        onClick={e => propsRef.current.onClick && propsRef.current.onClick(e)}\n      >\n        <span className=\"pointer-events-none absolute inset-0 rounded-[inherit]\" style={face} aria-hidden=\"true\" />\n        <span className=\"relative group-data-[pressed]/craft:scale-[0.96]\">{children}</span>\n        {pods.map(left => (\n          <span\n            key={left}\n            className=\"pointer-events-none absolute -z-10 -translate-x-1/2 rounded-[2px_2px_5px_5px]\"\n            style={{\n              left: `${left}%`,\n              top: `calc(100% - ${3 * k}px)`,\n              width: 14 * k,\n              height: 8 * k,\n              background: 'linear-gradient(180deg, var(--fly-pod), color-mix(in srgb, var(--fly-pod) 80%, #000))'\n            }}\n            aria-hidden=\"true\"\n          >\n            <span\n              className=\"absolute bottom-0 left-1/2 -translate-x-1/2 rounded-[2px_2px_1px_1px]\"\n              style={{\n                width: 7 * k,\n                height: 2 * k,\n                background: `color-mix(in srgb, var(--fly-glow) calc(var(--fly-thrust) * 100%), ${light ? '#52525b' : '#3f3f46'})`,\n                boxShadow: '0 1px 4px color-mix(in srgb, var(--fly-glow) calc(var(--fly-thrust) * 90%), transparent)'\n              }}\n            />\n          </span>\n        ))}\n      </button>\n      <svg\n        ref={cursorRef}\n        className=\"pointer-events-none absolute top-0 left-0 z-[3] hidden h-7 w-5 [filter:drop-shadow(0_1px_2px_rgba(0,0,0,0.28))] [will-change:transform] group-data-[carry]/fly:block\"\n        viewBox=\"0 0 24 34\"\n        aria-hidden=\"true\"\n      >\n        <path\n          d=\"M2.2 2.1v24.6l6.1-5.9 4.1 9.6 4.4-1.9-4.1-9.4h8.6z\"\n          fill=\"#0a0a0c\"\n          stroke=\"#fff\"\n          strokeWidth=\"1.9\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}