{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rush-type",
  "title": "Rush Type",
  "description": "A resting white word blasts through vertical scale motion, separating into energy-conserving RGB shutter trails before the next word resolves.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/rush-type/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  continueRender,\n  delayRender,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport interface RushTypeProps {\n  /** A short phrase. One whitespace-delimited word is shown per cycle. */\n  phrase?: string;\n  /** Cap height of the resting word in composition pixels. */\n  fontSize?: number;\n  fontFamily?: string;\n  fontWeight?: number;\n  color?: string;\n  backgroundColor?: string;\n  /** Authored vertical scale at the blast before perspective is applied. */\n  verticalStretch?: number;\n  /** 0 removes channel separation, 1 is the reference look, 2 exaggerates it. */\n  chromaticSpread?: number;\n  /** Readable resting phase, in frames. */\n  restDuration?: number;\n  /** Total near-point hang around the word swap, in frames. */\n  peakHoldDuration?: number;\n  /** Global playback multiplier. */\n  speed?: number;\n  className?: string;\n}\n\nexport type RushTypePhase = \"hang\" | \"arrive\" | \"rest\" | \"depart\";\n\nexport interface RushTypeFrameState {\n  phase: RushTypePhase;\n  wordIndex: number;\n  beforeWordIndex: number;\n  afterWordIndex: number;\n  cycle: number;\n  motion: number;\n  isResting: boolean;\n  swapAmount: number;\n  shutterRatios: [number, number, number];\n  thinAmount: number;\n  bloomAmount: number;\n  crtAmount: number;\n  groundAmount: number;\n}\n\nexport const rushTypeDefaultPhrase = \"gone before you look\";\n\nconst ARRIVE_FRAMES = 7;\nconst LEAVE_FRAMES = 4;\nconst DEFAULT_REST_FRAMES = 12;\nconst DEFAULT_HOLD_FRAMES = 3;\nconst SHUTTER_SECONDS = 0.04;\nconst SMEAR_GAIN = 1.6;\nconst SAMPLES = 28;\nconst SHUTTER_SHAPE = 0.85;\nconst ROLL_U = 0.38;\nconst LAG_U = 0.55;\nconst THIN = 0.55;\nconst BLOOM_GAIN = 0.5;\nconst BLOOM_SPREAD = 3.2;\nconst BLOOM_BIAS = 3.5;\nconst SPEED_GAIN = 0.95;\nconst EXPOSURE = 1.75;\nconst HORIZONTAL_STRETCH = 1.57;\nconst FOCAL = 1;\nconst ORBIT_DEPTH = 0.42;\nconst ORBIT_RISE = 0.16;\nconst ORBIT_YAW = 0.55;\nconst ORBIT_YAW_LAG = 0.6;\nconst ORBIT_PITCH = 0.26;\nconst ORBIT_PITCH_LAG = -0.9;\nconst ORBIT_HANG_ARC = 0.9;\nconst PIVOT_FRAC = 0.07;\nconst CRT_GAIN = 1;\nconst CRT_PITCH = 5;\nconst CRT_MASK = 0.16;\nconst CRT_SCAN_PITCH = 3;\nconst CRT_SCAN = 0.12;\nconst CRT_BEAM = 0.7;\nconst CRT_HUM = 0.035;\nconst CRT_HUM_SPEED = 0.14;\nconst GROUND_SPEED = 0.028;\nconst GLOW_SPREAD = 2.6;\nconst GLOW_MIN = 0.34;\nconst GLOW_MAX_Y = 1.2;\nconst SWAP_SPREAD = 0.35;\nconst SWAP_FLASH = 0.5;\n\nconst FALL = [1, 0.557, 0.121, 0.049, 0.014, 0];\nconst RISE = [0, 0.08, 0.8, 1];\nconst ORBIT_IN = [0, 0.5, 0.82, 0.95, 1, 1];\nconst ORBIT_OUT = [0, 0.05, 0.14, 0.38, 1];\nconst DRIFT_DIRECTION = [-1, 0.7, -0.45, 1];\nconst SWING_ARC = Math.PI - ORBIT_HANG_ARC;\n\nfunction parseHexColor(\n  value: string,\n  fallback: readonly [number, number, number],\n): [number, number, number] {\n  const hex = value.trim().replace(/^#/, \"\");\n  const expanded =\n    hex.length === 3 ? hex.replace(/./g, (char) => char + char) : hex;\n  if (!/^[0-9a-f]{6}$/i.test(expanded)) {\n    return [fallback[0], fallback[1], fallback[2]];\n  }\n  return [\n    Number.parseInt(expanded.slice(0, 2), 16) / 255,\n    Number.parseInt(expanded.slice(2, 4), 16) / 255,\n    Number.parseInt(expanded.slice(4, 6), 16) / 255,\n  ];\n}\n\nfunction positiveModulo(value: number, modulus: number): number {\n  return ((value % modulus) + modulus) % modulus;\n}\n\nfunction sampleTable(table: readonly number[], progress: number): number {\n  const last = table.length - 1;\n  const x = Math.min(Math.max(progress, 0), 1) * last;\n  const index = Math.min(Math.floor(x), last - 1);\n  return table[index] + (table[index + 1] - table[index]) * (x - index);\n}\n\nexport function normalizeRushTypePhrase(phrase: string): string[] {\n  const words = phrase.trim().split(/\\s+/).filter(Boolean);\n  return words.length > 0 ? words : rushTypeDefaultPhrase.split(\" \");\n}\n\nexport function getRushTypeCycleLength(\n  restDuration = DEFAULT_REST_FRAMES,\n  peakHoldDuration = DEFAULT_HOLD_FRAMES,\n): number {\n  return (\n    ARRIVE_FRAMES +\n    LEAVE_FRAMES +\n    Math.max(restDuration, 1) +\n    Math.max(peakHoldDuration, 0)\n  );\n}\n\nexport function getRushTypeDuration({\n  phrase = rushTypeDefaultPhrase,\n  restDuration = DEFAULT_REST_FRAMES,\n  peakHoldDuration = DEFAULT_HOLD_FRAMES,\n}: Pick<\n  RushTypeProps,\n  \"phrase\" | \"restDuration\" | \"peakHoldDuration\"\n> = {}): number {\n  return Math.round(\n    normalizeRushTypePhrase(phrase).length *\n      getRushTypeCycleLength(restDuration, peakHoldDuration),\n  );\n}\n\nexport const rushTypeLength = getRushTypeDuration();\n\nexport function getRushTypeShutterRatios(\n  chromaticSpread = 1,\n): [number, number, number] {\n  const spread = Math.max(chromaticSpread, 0);\n  return [0.62 ** spread, 1, 0.34 ** spread];\n}\n\ninterface TimelinePose {\n  phase: RushTypePhase;\n  p: number;\n  psi: number;\n  cycle: number;\n  wordIndex: number;\n}\n\ninterface TimelineOptions {\n  wordCount: number;\n  restDuration: number;\n  peakHoldDuration: number;\n}\n\nfunction getTimelinePose(\n  frame: number,\n  { wordCount, restDuration, peakHoldDuration }: TimelineOptions,\n): TimelinePose {\n  const safeWordCount = Math.max(Math.floor(wordCount), 1);\n  const safeRest = Math.max(restDuration, 1);\n  const safeHold = Math.max(peakHoldDuration, 0);\n  const halfHold = safeHold / 2;\n  const cycleLength = getRushTypeCycleLength(safeRest, safeHold);\n  const arriveEnd = halfHold + ARRIVE_FRAMES;\n  const restEnd = arriveEnd + safeRest;\n  const leaveEnd = restEnd + LEAVE_FRAMES;\n  const startOffset = arriveEnd + safeRest / 2;\n  const shifted = frame + startOffset;\n  const cycle = Math.floor(shifted / cycleLength);\n  const tau = positiveModulo(shifted, cycleLength);\n  const wordIndex = positiveModulo(cycle, safeWordCount);\n\n  if (tau < halfHold) {\n    return {\n      phase: \"hang\",\n      p: 1,\n      psi: ORBIT_HANG_ARC * (tau / Math.max(halfHold, 1e-6)),\n      cycle,\n      wordIndex,\n    };\n  }\n\n  if (tau < arriveEnd) {\n    const progress = (tau - halfHold) / ARRIVE_FRAMES;\n    return {\n      phase: \"arrive\",\n      p: sampleTable(FALL, progress),\n      psi: ORBIT_HANG_ARC + SWING_ARC * sampleTable(ORBIT_IN, progress),\n      cycle,\n      wordIndex,\n    };\n  }\n\n  if (tau < restEnd) {\n    return { phase: \"rest\", p: 0, psi: Math.PI, cycle, wordIndex };\n  }\n\n  if (tau < leaveEnd) {\n    const progress = (tau - restEnd) / LEAVE_FRAMES;\n    return {\n      phase: \"depart\",\n      p: sampleTable(RISE, progress),\n      psi: Math.PI + SWING_ARC * sampleTable(ORBIT_OUT, progress),\n      cycle,\n      wordIndex,\n    };\n  }\n\n  const progress = (tau - leaveEnd) / Math.max(halfHold, 1e-6);\n  return {\n    phase: \"hang\",\n    p: 1,\n    psi: 2 * Math.PI - ORBIT_HANG_ARC * (1 - progress),\n    cycle,\n    wordIndex,\n  };\n}\n\ninterface ComputedFrame {\n  before: TimelinePose;\n  here: TimelinePose;\n  after: TimelinePose;\n  swapU: number;\n  swapAmount: number;\n  shutterRatios: [number, number, number];\n}\n\nfunction computeFrame({\n  frame,\n  fps,\n  wordCount,\n  restDuration,\n  peakHoldDuration,\n  chromaticSpread,\n}: {\n  frame: number;\n  fps: number;\n  wordCount: number;\n  restDuration: number;\n  peakHoldDuration: number;\n  chromaticSpread: number;\n}): ComputedFrame {\n  const shutterFrames = SHUTTER_SECONDS * Math.max(fps, 1);\n  const halfShutter = shutterFrames / 2;\n  const options = { wordCount, restDuration, peakHoldDuration };\n  const before = getTimelinePose(frame - halfShutter, options);\n  const here = getTimelinePose(frame, options);\n  const after = getTimelinePose(frame + halfShutter, options);\n  const cycleLength = getRushTypeCycleLength(restDuration, peakHoldDuration);\n  const halfHold = Math.max(peakHoldDuration, 0) / 2;\n  const arriveEnd = halfHold + ARRIVE_FRAMES;\n  const startOffset = arriveEnd + Math.max(restDuration, 1) / 2;\n  const shifted = frame + startOffset;\n  const nearestBoundary = Math.round(shifted / cycleLength) * cycleLength;\n  const boundaryDistance = shifted - nearestBoundary;\n  const crossesSwap = wordCount > 1 && after.cycle > before.cycle;\n  const swapU = crossesSwap\n    ? (after.cycle * cycleLength - shifted) / shutterFrames\n    : 2;\n  const swapAmount =\n    wordCount > 1\n      ? Math.max(0, 1 - (2 * boundaryDistance) ** 2 / shutterFrames ** 2)\n      : 0;\n\n  return {\n    before,\n    here,\n    after,\n    swapU,\n    swapAmount,\n    shutterRatios: getRushTypeShutterRatios(chromaticSpread),\n  };\n}\n\nexport function getRushTypeFrameState({\n  frame,\n  fps = 30,\n  phrase = rushTypeDefaultPhrase,\n  restDuration = DEFAULT_REST_FRAMES,\n  peakHoldDuration = DEFAULT_HOLD_FRAMES,\n  chromaticSpread = 1,\n  speed = 1,\n}: {\n  frame: number;\n  fps?: number;\n} & Pick<\n  RushTypeProps,\n  \"phrase\" | \"restDuration\" | \"peakHoldDuration\" | \"chromaticSpread\" | \"speed\"\n>): RushTypeFrameState {\n  const words = normalizeRushTypePhrase(phrase);\n  const computed = computeFrame({\n    frame: frame * Math.max(speed, 0),\n    fps,\n    wordCount: words.length,\n    restDuration,\n    peakHoldDuration,\n    chromaticSpread,\n  });\n  const motion = computed.here.p;\n\n  return {\n    phase: computed.here.phase,\n    wordIndex: computed.here.wordIndex,\n    beforeWordIndex: computed.before.wordIndex,\n    afterWordIndex: computed.after.wordIndex,\n    cycle: computed.here.cycle,\n    motion,\n    isResting:\n      computed.here.phase === \"rest\" &&\n      computed.before.p === 0 &&\n      computed.after.p === 0,\n    swapAmount: computed.swapAmount,\n    shutterRatios: computed.shutterRatios,\n    thinAmount: THIN * motion,\n    bloomAmount: BLOOM_GAIN * motion,\n    crtAmount: CRT_GAIN * motion,\n    groundAmount: GROUND_SPEED * motion,\n  };\n}\n\nfunction orbitTurn(psi: number, amplitude: number, lag: number): number {\n  const pin = Math.sin(lag);\n  return (amplitude * (Math.sin(psi - lag) - pin)) / (1 + Math.abs(pin));\n}\n\nconst VERTEX_SHADER = `\nattribute vec2 aPos;\nvarying vec2 vUv;\nvoid main() {\n  vUv = aPos * 0.5 + 0.5;\n  gl_Position = vec4(aPos, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\nvarying vec2 vUv;\n\nuniform sampler2D uText;\nuniform vec2 uRes;\nuniform vec2 uHalfPx;\nuniform float uAtlasRows;\nuniform float uSx;\nuniform vec3 uSyQ;\nuniform float uCenterY;\nuniform float uSwapU;\nuniform float uHalfA;\nuniform float uHalfB;\nuniform vec3 uK;\nuniform vec2 uSwapScl;\nuniform float uShape;\nuniform float uRoll;\nuniform float uLag;\nuniform float uThin;\nuniform float uBloom;\nuniform float uGain;\nuniform float uExp;\nuniform float uFocal;\nuniform vec3 uPos;\nuniform vec4 uRot;\nuniform float uCrt;\nuniform float uTime;\nuniform vec4 uGlow;\nuniform float uGlowAmp;\nuniform vec3 uColor;\nuniform vec3 uBg;\n\n#define SAMPLES ${SAMPLES}\n#define BLOOM_TAPS 4\n#define BLOOM_SPREAD ${BLOOM_SPREAD.toFixed(3)}\n#define BLOOM_BIAS ${BLOOM_BIAS.toFixed(3)}\n#define TAU 6.2831853\n#define CRT_PITCH ${CRT_PITCH.toFixed(3)}\n#define CRT_MASK ${CRT_MASK.toFixed(4)}\n#define CRT_SCAN_PITCH ${CRT_SCAN_PITCH.toFixed(3)}\n#define CRT_SCAN ${CRT_SCAN.toFixed(4)}\n#define CRT_BEAM ${CRT_BEAM.toFixed(4)}\n#define CRT_HUM ${CRT_HUM.toFixed(4)}\n#define CRT_HUM_SPEED ${CRT_HUM_SPEED.toFixed(4)}\n\nvec2 atlasUv(float sy, float halfIdx, vec2 wordPoint, out float inside) {\n  float sx = max(uSx, 1e-4);\n  float sv = max(abs(sy), 1e-4);\n  vec2 q = vec2(\n    (wordPoint.x / sx + uHalfPx.x * 0.5) / uHalfPx.x,\n    (wordPoint.y / sv + uHalfPx.y * 0.5) / uHalfPx.y\n  );\n  inside = step(0.0, q.x) * step(q.x, 1.0) * step(0.0, q.y) * step(q.y, 1.0);\n  return vec2(\n    clamp(q.x, 0.0, 1.0),\n    (clamp(q.y, 0.0, 1.0) + halfIdx) / uAtlasRows\n  );\n}\n\nfloat tap(float sy, float halfIdx, vec2 wordPoint, float front) {\n  float inside;\n  vec2 texturePoint = atlasUv(sy, halfIdx, wordPoint, inside);\n  float value = texture2D(uText, texturePoint).r * inside * front;\n  return value * mix(1.0, value, uThin);\n}\n\nfloat tapBlur(float sy, float halfIdx, vec2 wordPoint, float front) {\n  float inside;\n  vec2 texturePoint = atlasUv(sy, halfIdx, wordPoint, inside);\n  return texture2D(uText, texturePoint, BLOOM_BIAS).r * inside * front;\n}\n\nvoid main() {\n  vec2 pixel = vUv * uRes;\n  vec2 center = vec2(uRes.x * 0.5, uRes.y * uCenterY);\n  vec2 screenPoint = pixel - center;\n\n  float sinYaw = uRot.x;\n  float cosYaw = uRot.y;\n  float sinPitch = uRot.z;\n  float cosPitch = uRot.w;\n  float a1 = -(screenPoint.x * sinYaw + uFocal * cosYaw);\n  float b1 = sinPitch * (screenPoint.x * cosYaw - uFocal * sinYaw);\n  float c1 = uFocal * uPos.x - screenPoint.x * uPos.z;\n  float a2 = -screenPoint.y * sinYaw;\n  float b2 = screenPoint.y * sinPitch * cosYaw - uFocal * cosPitch;\n  float c2 = uFocal * uPos.y - screenPoint.y * uPos.z;\n  float determinant = a1 * b2 - a2 * b1;\n  float inverse = 1.0 / (abs(determinant) < 1e-4 ? 1e-4 : determinant);\n  float a = (c1 * b2 - c2 * b1) * inverse;\n  float b = (a1 * c2 - a2 * c1) * inverse;\n  float cameraZ = uPos.z - a * sinYaw + b * sinPitch * cosYaw;\n  vec2 wordPoint = vec2(a, b);\n  float front = step(uFocal * 0.05, cameraZ);\n  vec3 accumulated = vec3(0.0);\n\n  if (abs(uSyQ.y) + abs(uSyQ.z) < 1e-5) {\n    accumulated = vec3(tap(uSyQ.x, uHalfA, wordPoint, front));\n  } else {\n    float offset =\n      uLag * (wordPoint.x / uRes.x) - uRoll * (screenPoint.y / uRes.y);\n    float weightSum = 0.0;\n\n    for (int i = 0; i < SAMPLES; i++) {\n      float u = float(i) / float(SAMPLES - 1) - 0.5;\n      float weight = 1.0 - uShape * abs(u) * 2.0;\n      weightSum += weight;\n      float shiftedU = u + offset;\n      vec3 shutterU = shiftedU * uK;\n      vec3 sy = uSyQ.x + uSyQ.y * shutterU + uSyQ.z * shutterU * shutterU;\n      accumulated.r += weight * tap(\n        sy.r * (shutterU.r < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.r < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n      accumulated.g += weight * tap(\n        sy.g * (shutterU.g < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.g < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n      accumulated.b += weight * tap(\n        sy.b * (shutterU.b < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.b < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n    }\n\n    accumulated /= max(weightSum, 1e-4);\n\n    vec3 halo = vec3(0.0);\n    for (int j = 0; j < BLOOM_TAPS; j++) {\n      float u =\n        (float(j) / float(BLOOM_TAPS - 1) - 0.5) * BLOOM_SPREAD + offset;\n      vec3 shutterU = u * uK;\n      vec3 sy = uSyQ.x + uSyQ.y * shutterU + uSyQ.z * shutterU * shutterU;\n      halo.r += tapBlur(\n        sy.r * (shutterU.r < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.r < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n      halo.g += tapBlur(\n        sy.g * (shutterU.g < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.g < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n      halo.b += tapBlur(\n        sy.b * (shutterU.b < uSwapU ? uSwapScl.x : uSwapScl.y),\n        shutterU.b < uSwapU ? uHalfA : uHalfB,\n        wordPoint,\n        front\n      );\n    }\n    accumulated += halo * (uBloom / float(BLOOM_TAPS));\n    accumulated *= uGain;\n  }\n\n  accumulated =\n    (1.0 - exp(-accumulated * uExp)) / (1.0 - exp(-uExp));\n\n  vec2 glowDistance =\n    (pixel - uGlow.xy) / max(uGlow.zw, vec2(1.0));\n  float ground =\n    uGlowAmp * (1.0 - smoothstep(0.0, 1.0, length(glowDistance)));\n  accumulated += ground * (1.0 - accumulated);\n\n  if (uCrt > 0.0) {\n    float luminance = max(max(accumulated.r, accumulated.g), accumulated.b);\n    vec3 maskWave = 0.5 + 0.5 * cos(\n      TAU * (pixel.x / CRT_PITCH - vec3(0.0, 0.33333, 0.66667))\n    );\n    vec3 mask = mix(vec3(1.0), maskWave * 2.0, CRT_MASK * uCrt);\n    float scanWave = 0.5 + 0.5 * cos(TAU * pixel.y / CRT_SCAN_PITCH);\n    float scan =\n      1.0 - CRT_SCAN * uCrt * (1.0 - CRT_BEAM * luminance) * (1.0 - scanWave);\n    float bar = 0.5 + 0.5 * cos(\n      TAU * (pixel.y / uRes.y - uTime * CRT_HUM_SPEED)\n    );\n    float hum = 1.0 + CRT_HUM * uCrt * (bar * 2.0 - 1.0);\n    accumulated *= mask * scan * hum;\n  }\n\n  gl_FragColor = vec4(uBg + (uColor - uBg) * accumulated, 1.0);\n}\n`;\n\nconst UNIFORM_NAMES = [\n  \"uText\",\n  \"uRes\",\n  \"uHalfPx\",\n  \"uAtlasRows\",\n  \"uSx\",\n  \"uSyQ\",\n  \"uCenterY\",\n  \"uSwapU\",\n  \"uHalfA\",\n  \"uHalfB\",\n  \"uK\",\n  \"uSwapScl\",\n  \"uShape\",\n  \"uRoll\",\n  \"uLag\",\n  \"uThin\",\n  \"uBloom\",\n  \"uGain\",\n  \"uExp\",\n  \"uFocal\",\n  \"uPos\",\n  \"uRot\",\n  \"uCrt\",\n  \"uTime\",\n  \"uGlow\",\n  \"uGlowAmp\",\n  \"uColor\",\n  \"uBg\",\n] as const;\n\ninterface GlState {\n  gl: WebGLRenderingContext;\n  program: WebGLProgram;\n  buffer: WebGLBuffer;\n  texture: WebGLTexture;\n  uniforms: Record<(typeof UNIFORM_NAMES)[number], WebGLUniformLocation | null>;\n  atlasWidth: number;\n  halfHeight: number;\n  textureCapHeight: number;\n  maxTextureSize: number;\n  scratch: HTMLCanvasElement;\n  atlasKey: string;\n  atlasRows: number;\n  wordHalfWidths: number[];\n  wordHalfWidth: number;\n}\n\nfunction compileShader(\n  gl: WebGLRenderingContext,\n  type: number,\n  source: string,\n): WebGLShader {\n  const shader = gl.createShader(type);\n  if (!shader) throw new Error(\"RushType could not create a WebGL shader\");\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    const message = gl.getShaderInfoLog(shader) ?? \"Unknown shader error\";\n    gl.deleteShader(shader);\n    throw new Error(`RushType shader compilation failed: ${message}`);\n  }\n  return shader;\n}\n\nfunction createGlState(canvas: HTMLCanvasElement): GlState | null {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: false,\n    antialias: false,\n    depth: false,\n    stencil: false,\n    preserveDrawingBuffer: false,\n    powerPreference: \"high-performance\",\n  });\n  if (!gl) return null;\n\n  const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!program) throw new Error(\"RushType could not create a WebGL program\");\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.bindAttribLocation(program, 0, \"aPos\");\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    const message = gl.getProgramInfoLog(program) ?? \"Unknown link error\";\n    gl.deleteProgram(program);\n    throw new Error(`RushType shader linking failed: ${message}`);\n  }\n\n  const buffer = gl.createBuffer();\n  const texture = gl.createTexture();\n  if (!buffer || !texture) {\n    throw new Error(\"RushType could not allocate WebGL resources\");\n  }\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(\n    gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 3, -1, -1, 3]),\n    gl.STATIC_DRAW,\n  );\n\n  const atlasWidth = canvas.width >= 1800 ? 4096 : 2048;\n  const halfHeight = atlasWidth / 4;\n  const textureCapHeight = Math.round(halfHeight * 0.64);\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(\n    gl.TEXTURE_2D,\n    gl.TEXTURE_MIN_FILTER,\n    gl.LINEAR_MIPMAP_LINEAR,\n  );\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\n  const scratch = document.createElement(\"canvas\");\n  scratch.width = atlasWidth;\n  scratch.height = halfHeight;\n  const uniforms = Object.fromEntries(\n    UNIFORM_NAMES.map((name) => [name, gl.getUniformLocation(program, name)]),\n  ) as GlState[\"uniforms\"];\n  gl.clearColor(0, 0, 0, 1);\n\n  return {\n    gl,\n    program,\n    buffer,\n    texture,\n    uniforms,\n    atlasWidth,\n    halfHeight,\n    textureCapHeight,\n    maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,\n    scratch,\n    atlasKey: \"\",\n    atlasRows: 2,\n    wordHalfWidths: [],\n    wordHalfWidth: 1,\n  };\n}\n\nfunction resolveFontFamily(fontFamily: string): string {\n  if (typeof document === \"undefined\") return fontFamily;\n  const probe = document.createElement(\"span\");\n  probe.style.cssText =\n    \"position:absolute;visibility:hidden;pointer-events:none\";\n  probe.style.fontFamily = fontFamily;\n  probe.textContent = \"Ag\";\n  document.body.appendChild(probe);\n  const resolved = getComputedStyle(probe).fontFamily;\n  probe.remove();\n  return resolved || fontFamily;\n}\n\nfunction rasterWord(\n  state: GlState,\n  word: string,\n  row: number,\n  fontFamily: string,\n  fontWeight: number,\n): number {\n  const context = state.scratch.getContext(\"2d\");\n  if (!context) return 1;\n  context.fillStyle = \"#000\";\n  context.fillRect(0, 0, state.atlasWidth, state.halfHeight);\n\n  let rasterSize = state.textureCapHeight * 1.4;\n  context.font = `${fontWeight} ${rasterSize}px ${fontFamily}`;\n  const capHeight =\n    context.measureText(\"H\").actualBoundingBoxAscent || rasterSize * 0.72;\n  rasterSize *= state.textureCapHeight / capHeight;\n  context.font = `${fontWeight} ${rasterSize}px ${fontFamily}`;\n\n  const maxWidth = state.atlasWidth * 0.92;\n  const measured = context.measureText(word).width;\n  if (measured > maxWidth) {\n    rasterSize *= maxWidth / measured;\n    context.font = `${fontWeight} ${rasterSize}px ${fontFamily}`;\n  }\n\n  context.fillStyle = \"#fff\";\n  context.textAlign = \"center\";\n  context.textBaseline = \"alphabetic\";\n  context.fillText(\n    word,\n    state.atlasWidth / 2,\n    state.halfHeight / 2 + state.textureCapHeight / 2,\n  );\n  const halfWidth = context.measureText(word).width / 2;\n\n  const { gl } = state;\n  gl.bindTexture(gl.TEXTURE_2D, state.texture);\n  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n  gl.texSubImage2D(\n    gl.TEXTURE_2D,\n    0,\n    0,\n    row * state.halfHeight,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    state.scratch,\n  );\n  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n  return halfWidth;\n}\n\nfunction nextPowerOfTwo(value: number): number {\n  return 2 ** Math.ceil(Math.log2(Math.max(value, 2)));\n}\n\nexport function getRushTypePhraseAtlasRows({\n  wordCount,\n  rowHeight,\n  maxTextureSize,\n}: {\n  wordCount: number;\n  rowHeight: number;\n  maxTextureSize: number;\n}): number | null {\n  const rows = nextPowerOfTwo(wordCount);\n  return rowHeight * rows <= maxTextureSize ? rows : null;\n}\n\nfunction allocateAtlas(state: GlState, rows: number): void {\n  const { gl } = state;\n  gl.bindTexture(gl.TEXTURE_2D, state.texture);\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    state.atlasWidth,\n    state.halfHeight * rows,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    null,\n  );\n  state.atlasRows = rows;\n}\n\nfunction updateAtlas(\n  state: GlState,\n  words: string[],\n  firstWordIndex: number,\n  secondWordIndex: number,\n  fontFamily: string,\n  fontWeight: number,\n): { firstRow: number; secondRow: number } {\n  const phraseRows = getRushTypePhraseAtlasRows({\n    wordCount: words.length,\n    rowHeight: state.halfHeight,\n    maxTextureSize: state.maxTextureSize,\n  });\n\n  if (phraseRows !== null) {\n    const atlasKey = `phrase\\u0000${fontFamily}\\u0000${fontWeight}\\u0000${words.join(\"\\u0000\")}`;\n    if (state.atlasKey !== atlasKey) {\n      allocateAtlas(state, phraseRows);\n      state.wordHalfWidths = words.map((word, row) =>\n        rasterWord(state, word, row, fontFamily, fontWeight),\n      );\n      state.gl.generateMipmap(state.gl.TEXTURE_2D);\n      state.atlasKey = atlasKey;\n    }\n    state.wordHalfWidth = Math.max(\n      state.wordHalfWidths[firstWordIndex] ?? 1,\n      state.wordHalfWidths[secondWordIndex] ?? 1,\n    );\n    return { firstRow: firstWordIndex, secondRow: secondWordIndex };\n  }\n\n  const firstWord = words[firstWordIndex] ?? words[0];\n  const secondWord = words[secondWordIndex] ?? words[0];\n  const atlasKey = `pair\\u0000${fontFamily}\\u0000${fontWeight}\\u0000${firstWord}\\u0000${secondWord}`;\n  if (state.atlasKey !== atlasKey) {\n    allocateAtlas(state, 2);\n    const firstWidth = rasterWord(state, firstWord, 0, fontFamily, fontWeight);\n    const secondWidth = rasterWord(\n      state,\n      secondWord,\n      1,\n      fontFamily,\n      fontWeight,\n    );\n    state.gl.generateMipmap(state.gl.TEXTURE_2D);\n    state.wordHalfWidths = [firstWidth, secondWidth];\n    state.atlasKey = atlasKey;\n  }\n  state.wordHalfWidth = Math.max(...state.wordHalfWidths, 1);\n  return { firstRow: 0, secondRow: 1 };\n}\n\nfunction destroyGlState(state: GlState): void {\n  const { gl } = state;\n  gl.deleteProgram(state.program);\n  gl.deleteBuffer(state.buffer);\n  gl.deleteTexture(state.texture);\n}\n\nfunction drawRushType({\n  state,\n  computed,\n  words,\n  fontSize,\n  fontFamily,\n  fontWeight,\n  verticalStretch,\n  textColor,\n  backgroundColor,\n  frame,\n  fps,\n}: {\n  state: GlState;\n  computed: ComputedFrame;\n  words: string[];\n  fontSize: number;\n  fontFamily: string;\n  fontWeight: number;\n  verticalStretch: number;\n  textColor: [number, number, number];\n  backgroundColor: [number, number, number];\n  frame: number;\n  fps: number;\n}): void {\n  const { gl, uniforms } = state;\n  const { firstRow, secondRow } = updateAtlas(\n    state,\n    words,\n    computed.before.wordIndex,\n    computed.after.wordIndex,\n    fontFamily,\n    fontWeight,\n  );\n\n  const unit = Math.max(fontSize, 1) / state.textureCapHeight;\n  const scaleY = (pose: TimelinePose) =>\n    unit * (1 + pose.p * (Math.max(verticalStretch, 1) - 1));\n  const scaleBefore = scaleY(computed.before);\n  const scaleCurrent = scaleY(computed.here);\n  const scaleAfter = scaleY(computed.after);\n  const quadraticA = scaleCurrent;\n  const quadraticC = 2 * (scaleAfter + scaleBefore - 2 * scaleCurrent);\n  const tangent = scaleAfter - scaleBefore;\n  const peakSmear = SMEAR_GAIN * computed.here.p * scaleCurrent;\n  const magnitude = Math.sqrt(tangent * tangent + peakSmear * peakSmear);\n  const quadraticB = tangent < 0 ? -magnitude : magnitude;\n  const scaleX = unit * (1 + computed.here.p * (HORIZONTAL_STRETCH - 1));\n\n  const direction =\n    DRIFT_DIRECTION[\n      positiveModulo(computed.here.cycle, DRIFT_DIRECTION.length)\n    ] ?? 1;\n  const psi = computed.here.psi * Math.sign(direction || 1);\n  const orbitAmplitude = Math.abs(direction);\n  const focal =\n    state.scratch.height > 0 ? state.gl.drawingBufferHeight * FOCAL : 1;\n  const z = focal * (1 - ORBIT_DEPTH * (1 + Math.cos(psi)) * 0.5);\n  const x = 0;\n  const y = focal * ORBIT_RISE * orbitAmplitude * Math.sin(psi);\n  const yaw = orbitTurn(psi, ORBIT_YAW, ORBIT_YAW_LAG);\n  const pitch = orbitTurn(psi, ORBIT_PITCH, ORBIT_PITCH_LAG);\n  const pivot = PIVOT_FRAC * computed.here.p;\n  const perspective = focal / z;\n  const glowX = gl.drawingBufferWidth / 2 + (focal * x) / z;\n  const glowY = gl.drawingBufferHeight * (0.5 + pivot) + (focal * y) / z;\n  const wordWidth = state.wordHalfWidth * scaleX * perspective;\n  const wordHeight = state.textureCapHeight * scaleCurrent * perspective;\n\n  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n  // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL method, not a React hook\n  gl.useProgram(state.program);\n  gl.bindBuffer(gl.ARRAY_BUFFER, state.buffer);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, state.texture);\n  gl.uniform1i(uniforms.uText, 0);\n  gl.uniform2f(uniforms.uRes, gl.drawingBufferWidth, gl.drawingBufferHeight);\n  gl.uniform2f(uniforms.uHalfPx, state.atlasWidth, state.halfHeight);\n  gl.uniform1f(uniforms.uAtlasRows, state.atlasRows);\n  gl.uniform1f(uniforms.uSx, scaleX);\n  gl.uniform3f(uniforms.uSyQ, quadraticA, quadraticB, quadraticC);\n  gl.uniform1f(uniforms.uCenterY, 0.5 + pivot);\n  gl.uniform1f(uniforms.uSwapU, computed.swapU);\n  gl.uniform1f(uniforms.uHalfA, firstRow);\n  gl.uniform1f(uniforms.uHalfB, secondRow);\n  gl.uniform3f(\n    uniforms.uK,\n    computed.shutterRatios[0],\n    computed.shutterRatios[1],\n    computed.shutterRatios[2],\n  );\n  gl.uniform2f(\n    uniforms.uSwapScl,\n    1 + SWAP_SPREAD * computed.swapAmount,\n    1 - SWAP_SPREAD * computed.swapAmount,\n  );\n  gl.uniform1f(uniforms.uShape, SHUTTER_SHAPE);\n  gl.uniform1f(uniforms.uRoll, ROLL_U);\n  gl.uniform1f(uniforms.uLag, LAG_U);\n  gl.uniform1f(uniforms.uThin, THIN * computed.here.p);\n  gl.uniform1f(uniforms.uBloom, BLOOM_GAIN * computed.here.p);\n  gl.uniform1f(\n    uniforms.uGain,\n    (1 + SPEED_GAIN * computed.here.p) * (1 + SWAP_FLASH * computed.swapAmount),\n  );\n  gl.uniform1f(uniforms.uExp, EXPOSURE);\n  gl.uniform1f(uniforms.uFocal, focal);\n  gl.uniform3f(uniforms.uPos, x, y, z);\n  gl.uniform4f(\n    uniforms.uRot,\n    Math.sin(yaw),\n    Math.cos(yaw),\n    Math.sin(pitch),\n    Math.cos(pitch),\n  );\n  gl.uniform1f(uniforms.uCrt, CRT_GAIN * computed.here.p);\n  gl.uniform1f(uniforms.uTime, frame / Math.max(fps, 1));\n  gl.uniform4f(\n    uniforms.uGlow,\n    glowX,\n    glowY,\n    Math.max(wordWidth * GLOW_SPREAD, gl.drawingBufferWidth * GLOW_MIN),\n    Math.min(\n      Math.max(wordHeight * GLOW_SPREAD, gl.drawingBufferHeight * GLOW_MIN),\n      gl.drawingBufferHeight * GLOW_MAX_Y,\n    ),\n  );\n  gl.uniform1f(uniforms.uGlowAmp, GROUND_SPEED * computed.here.p);\n  gl.uniform3f(uniforms.uColor, textColor[0], textColor[1], textColor[2]);\n  gl.uniform3f(\n    uniforms.uBg,\n    backgroundColor[0],\n    backgroundColor[1],\n    backgroundColor[2],\n  );\n  gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1);\n  gl.clear(gl.COLOR_BUFFER_BIT);\n  gl.drawArrays(gl.TRIANGLES, 0, 3);\n}\n\nexport function RushType({\n  phrase = rushTypeDefaultPhrase,\n  fontSize = 68,\n  fontFamily = \"Arial, Helvetica, sans-serif\",\n  fontWeight = 400,\n  color = \"#ffffff\",\n  backgroundColor = \"#000000\",\n  verticalStretch = 7,\n  chromaticSpread = 1,\n  restDuration = DEFAULT_REST_FRAMES,\n  peakHoldDuration = DEFAULT_HOLD_FRAMES,\n  speed = 1,\n  className,\n}: RushTypeProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const stateRef = useRef<GlState | null>(null);\n  const [fontReady, setFontReady] = useState(false);\n  const [webGlFailed, setWebGlFailed] = useState(false);\n  const [renderHandle] = useState(() => delayRender(\"rush-type: first frame\"));\n  const continuedRef = useRef(false);\n  const words = useMemo(() => normalizeRushTypePhrase(phrase), [phrase]);\n  const resolvedFontFamily = useMemo(\n    () => resolveFontFamily(fontFamily),\n    [fontFamily],\n  );\n  const textColor = useMemo(() => parseHexColor(color, [1, 1, 1]), [color]);\n  const frameColor = useMemo(\n    () => parseHexColor(backgroundColor, [0, 0, 0]),\n    [backgroundColor],\n  );\n  const effectiveFrame = frame * Math.max(speed, 0);\n  const computed = computeFrame({\n    frame: effectiveFrame,\n    fps,\n    wordCount: words.length,\n    restDuration,\n    peakHoldDuration,\n    chromaticSpread,\n  });\n\n  useEffect(() => {\n    let cancelled = false;\n    setFontReady(false);\n    const ready = () => {\n      if (!cancelled) setFontReady(true);\n    };\n    if (typeof document === \"undefined\" || !document.fonts) {\n      ready();\n      return () => {\n        cancelled = true;\n      };\n    }\n    document.fonts\n      .load(\n        `${Number(fontWeight) || 400} ${Math.max(fontSize, 1)}px ${fontFamily}`,\n      )\n      .then(ready, ready);\n    return () => {\n      cancelled = true;\n    };\n  }, [fontFamily, fontSize, fontWeight]);\n\n  useLayoutEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || stateRef.current || webGlFailed) return;\n    try {\n      const state = createGlState(canvas);\n      if (!state) {\n        setWebGlFailed(true);\n        return;\n      }\n      stateRef.current = state;\n    } catch {\n      setWebGlFailed(true);\n    }\n    return () => {\n      if (stateRef.current) destroyGlState(stateRef.current);\n      stateRef.current = null;\n    };\n  }, [webGlFailed]);\n\n  useLayoutEffect(() => {\n    const state = stateRef.current;\n    if (!state || !fontReady) return;\n    drawRushType({\n      state,\n      computed,\n      words,\n      fontSize,\n      fontFamily: resolvedFontFamily,\n      fontWeight: Number(fontWeight) || 400,\n      verticalStretch,\n      textColor,\n      backgroundColor: frameColor,\n      frame: effectiveFrame,\n      fps,\n    });\n    if (!continuedRef.current) {\n      continuedRef.current = true;\n      continueRender(renderHandle);\n    }\n  }, [\n    computed,\n    effectiveFrame,\n    fontReady,\n    fontSize,\n    fontWeight,\n    fps,\n    frameColor,\n    renderHandle,\n    resolvedFontFamily,\n    textColor,\n    verticalStretch,\n    words,\n  ]);\n\n  useEffect(() => {\n    if (!webGlFailed || continuedRef.current) return;\n    continuedRef.current = true;\n    continueRender(renderHandle);\n  }, [renderHandle, webGlFailed]);\n\n  const currentWord = words[computed.here.wordIndex] ?? words[0];\n\n  return (\n    <div\n      className={className}\n      role=\"img\"\n      aria-label={`${phrase}: words stretching into vertical motion blur`}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        backgroundColor,\n      }}\n    >\n      <canvas\n        ref={canvasRef}\n        width={width}\n        height={height}\n        style={{\n          display: webGlFailed ? \"none\" : \"block\",\n          width: \"100%\",\n          height: \"100%\",\n        }}\n      />\n      {webGlFailed ? (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color,\n            fontFamily,\n            fontSize,\n            fontWeight,\n          }}\n        >\n          {currentWord}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/rush-type.tsx"
    }
  ],
  "type": "registry:component"
}
