{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fog-text-reveal",
  "title": "Fog Text Reveal",
  "description": "Inline text that materializes through a soft, turbulent WebGL fog mask, inherits font and color from CSS, and cycles through an array of messages.",
  "dependencies": ["motion"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/primitives/texts/fog-text-reveal/index.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type ElementType,\n  type Ref,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nconst VERT = `\n  attribute vec2 aPosition;\n  attribute vec2 aUV;\n  varying vec2 vUV;\n  void main(){\n    vUV = aUV;\n    gl_Position = vec4(aPosition, 0.0, 1.0);\n  }\n`;\n\nconst FRAG = `\n  precision highp float;\n  uniform sampler2D uTex;\n  uniform vec2  uTexel;\n  uniform float uProgress;\n  uniform float uMaxBlur;\n  uniform vec3  uEdge;\n  uniform float uTime;\n  uniform float uAspect;\n  uniform float uSeed;\n  uniform vec2  uParTL;\n  uniform vec2  uParBR;\n  uniform vec2  uCursor;\n  uniform float uHover;\n  uniform float uReverse;\n  varying vec2 vUV;\n\n  float hash(vec2 p){ return fract(sin(dot(p, vec2(41.3, 289.1))) * 43758.5453); }\n  float noise(vec2 p){\n    vec2 i = floor(p), f = fract(p);\n    f = f*f*(3.0-2.0*f);\n    float a = hash(i), b = hash(i+vec2(1,0)), c = hash(i+vec2(0,1)), d = hash(i+vec2(1,1));\n    return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);\n  }\n\n  vec4 blurTex(vec2 uv, float radius){\n    if (radius < 0.35) return texture2D(uTex, uv);\n    vec2 r1 = uTexel * radius;\n    vec2 r2 = uTexel * radius * 2.0;\n    vec4 sum = texture2D(uTex, uv) * 1.0;\n    float wsum = 1.0;\n    for (int i = 0; i < 8; i++){\n      float a = float(i) * 0.785398;\n      vec2 dir = vec2(cos(a), sin(a));\n      sum += texture2D(uTex, uv + dir * r1) * 0.75; wsum += 0.75;\n      sum += texture2D(uTex, uv + dir * r2) * 0.5;  wsum += 0.5;\n    }\n    return sum / wsum;\n  }\n\n  float fbm(vec2 p){\n    float v = 0.0, amp = 0.5;\n    for (int i = 0; i < 4; i++){\n      v += amp * noise(p);\n      p *= 2.03;\n      amp *= 0.5;\n    }\n    return v;\n  }\n\n  void main(){\n    float region = smoothstep(0.35, 0.65, (vUV.x + vUV.y) * 0.5);\n    vec2 par = mix(uParTL, uParBR, region);\n    vec2 baseUV = vUV + par;\n\n    if (uProgress >= 0.999) { gl_FragColor = texture2D(uTex, baseUV); return; }\n\n    float p = uProgress * 1.3;\n\n    vec2 sd = vec2(uSeed * 1.7, uSeed * -1.3);\n    vec2 rc = (vUV - 0.5) * vec2(uAspect, 1.0);\n\n    float diag = (vUV.x + vUV.y) * 0.5;\n    diag = smoothstep(0.18, 0.82, diag);\n\n    diag = mix(diag, 1.0 - diag, uReverse);\n    diag += (fbm(vUV * 1.3 + sd) - 0.5) * 0.08;\n\n    vec2 warp = vec2(fbm(vUV * 3.2 + sd + uTime * 0.05 + 11.0),\n                     fbm(vUV * 3.2 - sd - uTime * 0.04 - 7.0)) - 0.5;\n    float turb = fbm(vUV * 5.5 + warp * 1.7 + sd + uTime * 0.06);\n\n    float stipple = noise(vUV * vec2(uAspect, 1.0) * 46.0 + sd * 3.0);\n\n    float mask = mix(diag, turb, 0.28);\n    mask = mix(mask, stipple, 0.14);\n\n    vec2 cur = (vUV - uCursor) * vec2(uAspect, 1.0);\n    float near = 1.0 - smoothstep(0.0, 0.32, length(cur));\n    mask -= near * uHover * 0.28;\n\n    float reveal = smoothstep(p + 0.22, p - 0.22, mask);\n    if (reveal <= 0.0) discard;\n\n    float blurAmt = smoothstep(p - 0.34, p + 0.22, mask);\n\n    vec2 drift = (-rc * 0.010 + vec2(0.0, 0.006)) * blurAmt;\n    float grow = 1.0 + 0.03 * blurAmt;\n    vec2 suv = (baseUV - 0.5) / grow + 0.5 + drift;\n\n    float radius = blurAmt * uMaxBlur;\n    vec4 tex = blurTex(suv, radius);\n\n    float fw = 0.30;\n    float flare = smoothstep(p - fw, p, mask) * smoothstep(p + fw, p, mask);\n    flare *= 1.0 - smoothstep(0.8, 1.0, uProgress);\n\n    float ab = flare * 2.0 * uTexel.x * uMaxBlur;\n    if (ab > 0.0001) {\n      tex.r = blurTex(suv + vec2(ab, 0.0), radius).r;\n      tex.b = blurTex(suv - vec2(ab, 0.0), radius).b;\n    }\n\n    vec4 wide = blurTex(suv, uMaxBlur * 1.3);\n    float halo = wide.a;\n\n    vec3 rgb = tex.rgb;\n    vec3 glow = mix(uEdge, vec3(1.0), 0.3);\n    rgb += glow * flare * (tex.a * 0.6 + halo * 0.5);\n    float alpha = max(tex.a * reveal, halo * flare * 0.5);\n\n    gl_FragColor = vec4(rgb, alpha);\n  }\n`;\n\nconst UNIFORM_NAMES = [\n  \"uTex\",\n  \"uTexel\",\n  \"uProgress\",\n  \"uMaxBlur\",\n  \"uEdge\",\n  \"uTime\",\n  \"uAspect\",\n  \"uSeed\",\n  \"uParTL\",\n  \"uParBR\",\n  \"uCursor\",\n  \"uHover\",\n  \"uReverse\",\n] as const;\n\nclass RevealGL {\n  readonly canvas: HTMLCanvasElement;\n  private readonly gl: WebGLRenderingContext | null;\n  private readonly prog: WebGLProgram | null = null;\n  private readonly quad: WebGLBuffer | null = null;\n  private readonly loc: Record<string, WebGLUniformLocation | null> = {};\n  private readonly aPos: number = 0;\n  private readonly aUV: number = 0;\n  private tex: WebGLTexture | null = null;\n  private texW = 1;\n  private texH = 1;\n  private readonly ok: boolean = false;\n\n  constructor() {\n    this.canvas = document.createElement(\"canvas\");\n    Object.assign(this.canvas.style, {\n      position: \"absolute\",\n      inset: \"0\",\n      width: \"100%\",\n      height: \"100%\",\n      display: \"block\",\n    });\n    const gl = this.canvas.getContext(\"webgl\", {\n      alpha: true,\n      premultipliedAlpha: false,\n    });\n    this.gl = gl;\n    if (!gl) {\n      return;\n    }\n\n    const prog = this.build(gl, VERT, FRAG);\n    if (!prog) {\n      return;\n    }\n    this.aPos = gl.getAttribLocation(prog, \"aPosition\");\n    this.aUV = gl.getAttribLocation(prog, \"aUV\");\n    for (const u of UNIFORM_NAMES) {\n      this.loc[u] = gl.getUniformLocation(prog, u);\n    }\n\n    const data = new Float32Array([\n      -1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0,\n    ]);\n    const quad = gl.createBuffer();\n    if (!quad) {\n      return;\n    }\n    gl.bindBuffer(gl.ARRAY_BUFFER, quad);\n    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n    this.prog = prog;\n    this.quad = quad;\n    this.ok = true;\n  }\n\n  get available() {\n    return this.ok;\n  }\n\n  private build(\n    gl: WebGLRenderingContext,\n    vs: string,\n    fs: string\n  ): WebGLProgram | null {\n    const compile = (type: number, src: string): WebGLShader | null => {\n      const sh = gl.createShader(type);\n      if (!sh) {\n        return null;\n      }\n      gl.shaderSource(sh, src);\n      gl.compileShader(sh);\n      if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {\n        return null;\n      }\n      return sh;\n    };\n\n    const vShader = compile(gl.VERTEX_SHADER, vs);\n    const fShader = compile(gl.FRAGMENT_SHADER, fs);\n    if (!(vShader && fShader)) {\n      return null;\n    }\n\n    const p = gl.createProgram();\n    if (!p) {\n      return null;\n    }\n    gl.attachShader(p, vShader);\n    gl.attachShader(p, fShader);\n    gl.linkProgram(p);\n    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {\n      return null;\n    }\n    return p;\n  }\n\n  setTexture(art: HTMLCanvasElement) {\n    const gl = this.gl;\n    if (!(this.ok && gl)) {\n      return;\n    }\n    if (this.tex) {\n      gl.deleteTexture(this.tex);\n    }\n    const tex = gl.createTexture();\n    if (!tex) {\n      return;\n    }\n    gl.bindTexture(gl.TEXTURE_2D, tex);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, art);\n    this.tex = tex;\n    this.texW = art.width;\n    this.texH = art.height;\n  }\n\n  resize(w: number, h: number, dpr: number) {\n    this.canvas.width = Math.max(1, Math.round(w * dpr));\n    this.canvas.height = Math.max(1, Math.round(h * dpr));\n    if (this.ok && this.gl) {\n      this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);\n    }\n  }\n\n  draw(\n    progress: number,\n    maxBlur: number,\n    edge: [number, number, number],\n    time: number,\n    aspect: number,\n    seed: number,\n    parTL: [number, number],\n    parBR: [number, number],\n    cursor: [number, number],\n    hover: number,\n    reverse: number\n  ) {\n    const gl = this.gl;\n    if (!(this.ok && gl && this.tex && this.prog && this.quad)) {\n      return;\n    }\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL call, not a React hook.\n    gl.useProgram(this.prog);\n    gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);\n    gl.enableVertexAttribArray(this.aPos);\n    gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 16, 0);\n    gl.enableVertexAttribArray(this.aUV);\n    gl.vertexAttribPointer(this.aUV, 2, gl.FLOAT, false, 16, 8);\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, this.tex);\n    gl.uniform1i(this.loc.uTex, 0);\n    gl.uniform2f(this.loc.uTexel, 1 / this.texW, 1 / this.texH);\n    gl.uniform1f(this.loc.uProgress, progress);\n    gl.uniform1f(this.loc.uMaxBlur, maxBlur);\n    gl.uniform3f(this.loc.uEdge, edge[0], edge[1], edge[2]);\n    gl.uniform1f(this.loc.uTime, time);\n    gl.uniform1f(this.loc.uAspect, aspect);\n    gl.uniform1f(this.loc.uSeed, seed);\n    gl.uniform2f(this.loc.uParTL, parTL[0], parTL[1]);\n    gl.uniform2f(this.loc.uParBR, parBR[0], parBR[1]);\n    gl.uniform2f(this.loc.uCursor, cursor[0], cursor[1]);\n    gl.uniform1f(this.loc.uHover, hover);\n    gl.uniform1f(this.loc.uReverse, reverse);\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n  }\n\n  destroy() {\n    const gl = this.gl;\n    if (!(this.ok && gl)) {\n      return;\n    }\n    if (this.tex) {\n      gl.deleteTexture(this.tex);\n    }\n    gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n  }\n}\n\ntype TextAlign = \"left\" | \"center\" | \"right\";\n\nfunction normalizeAlign(value: string): TextAlign {\n  if (value === \"right\" || value === \"end\") {\n    return \"right\";\n  }\n  if (value === \"center\") {\n    return \"center\";\n  }\n  return \"left\";\n}\n\ninterface TextTexture {\n  align: TextAlign;\n  boxH: number;\n  boxW: number;\n  dpr: number;\n  fill: string;\n  font: string;\n  /** Line height in px, or 0 to derive from the font's natural metrics. */\n  lineHeight: number;\n  padX: number;\n  padY: number;\n  text: string;\n}\n\nfunction renderTextTexture(o: TextTexture): HTMLCanvasElement | null {\n  const cssW = Math.max(1, Math.round(o.boxW + o.padX * 2));\n  const cssH = Math.max(1, Math.round(o.boxH + o.padY * 2));\n\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = Math.round(cssW * o.dpr);\n  canvas.height = Math.round(cssH * o.dpr);\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    return null;\n  }\n  ctx.scale(o.dpr, o.dpr);\n  ctx.font = o.font;\n  ctx.fillStyle = o.fill;\n  ctx.textBaseline = \"alphabetic\";\n  ctx.textAlign = o.align;\n\n  const m = ctx.measureText(\"Ag\");\n  const ascent = m.fontBoundingBoxAscent;\n  const descent = m.fontBoundingBoxDescent;\n  const lineH = o.lineHeight > 0 ? o.lineHeight : ascent + descent;\n\n  const lines = o.text.split(\"\\n\");\n  const blockH = lines.length * lineH;\n  const startY = (o.boxH - blockH) / 2;\n  let x = o.padX;\n  if (o.align === \"right\") {\n    x = o.padX + o.boxW;\n  } else if (o.align === \"center\") {\n    x = o.padX + o.boxW / 2;\n  }\n\n  lines.forEach((line, i) => {\n    // Half-leading model so GL glyphs land where the in-flow sizer glyphs sit.\n    const baseline =\n      o.padY + startY + i * lineH + (lineH + ascent - descent) / 2;\n    ctx.fillText(line, x, baseline);\n  });\n\n  return canvas;\n}\n\nlet colorProbe: CanvasRenderingContext2D | null = null;\n\nfunction cssColorToRgb(css: string): [number, number, number] {\n  if (!colorProbe) {\n    const c = document.createElement(\"canvas\");\n    c.width = 1;\n    c.height = 1;\n    colorProbe = c.getContext(\"2d\", { willReadFrequently: true });\n  }\n  const ctx = colorProbe;\n  if (!ctx) {\n    return [1, 1, 1];\n  }\n  ctx.clearRect(0, 0, 1, 1);\n  ctx.fillStyle = css;\n  ctx.fillRect(0, 0, 1, 1);\n  const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;\n  return [r / 255, g / 255, b / 255];\n}\n\nfunction springStep(\n  pos: number,\n  vel: number,\n  target: number,\n  k: number,\n  damp: number,\n  dt: number\n): [number, number] {\n  const c = 2 * Math.sqrt(k) * damp;\n  const accel = -k * (pos - target) - c * vel;\n  const v = vel + accel * dt;\n  return [pos + v * dt, v];\n}\n\nconst OUT_HOLD_MS = 80;\nconst K_IN = 16;\nconst K_OUT = 22;\nconst DAMP = 1.12;\nconst REVEALED_AT = 0.95;\nconst GONE_AT = 0.02;\nconst PARALLAX_AMP = 0.006;\nconst TL_ANCHOR: [number, number] = [0.16, 0.18];\nconst BR_ANCHOR: [number, number] = [0.84, 0.82];\n\nexport interface FogTextRevealProps\n  extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n  /**\n   * Render as a different element instead of `span`.\n   * @default \"span\"\n   */\n  as?: ElementType;\n  /**\n   * How long each message stays fully revealed before clearing, in ms.\n   * @default 320\n   */\n  holdDuration?: number;\n  /**\n   * Cycle back to the first message after the last. When `false`, the final\n   * message reveals once and stays.\n   * @default true\n   */\n  loop?: boolean;\n  /**\n   * Maximum blur radius of the fog mask, in texels.\n   * @default 16\n   */\n  maxBlur?: number;\n  /** Forwarded ref to the root element. */\n  ref?: Ref<HTMLElement>;\n  /**\n   * Wait until the text scrolls into view before animating.\n   * @default true\n   */\n  startOnView?: boolean;\n  /**\n   * Text to reveal. Pass an array to cycle through messages; the element\n   * reserves a stable width sized to the widest one so cycling never shifts\n   * surrounding layout. Font and color are inherited from CSS.\n   */\n  text: string | string[];\n}\n\nfunction FogTextReveal({\n  text,\n  as: Component = \"span\",\n  loop = true,\n  startOnView = true,\n  holdDuration = 320,\n  maxBlur = 16,\n  className,\n  ref,\n  ...props\n}: FogTextRevealProps) {\n  const hostRef = useRef<HTMLElement | null>(null);\n  const sizerRef = useRef<HTMLSpanElement>(null);\n  const [fallback, setFallback] = useState(false);\n  const reduced = useReducedMotion();\n  const texts = useMemo(() => (Array.isArray(text) ? text : [text]), [text]);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    const sizer = sizerRef.current;\n    if (!(host && sizer) || texts.length === 0 || reduced) {\n      return;\n    }\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n    const gl = new RevealGL();\n    if (!gl.available) {\n      setFallback(true);\n      return;\n    }\n    gl.canvas.style.pointerEvents = \"none\";\n    gl.canvas.setAttribute(\"aria-hidden\", \"true\");\n    host.appendChild(gl.canvas);\n\n    const measureCanvas = document.createElement(\"canvas\");\n    const mctx = measureCanvas.getContext(\"2d\");\n\n    let font = \"16px serif\";\n    let fill = \"#000\";\n    let edge: [number, number, number] = [1, 1, 1];\n    let align: TextAlign = \"left\";\n    let lineH = 0;\n    let W = 1;\n    let H = 1;\n    let padX = 0;\n    let padY = 0;\n    let quadW = 1;\n    let quadH = 1;\n    let disposed = false;\n\n    const measure = () => {\n      const cs = getComputedStyle(host);\n      font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;\n      fill = cs.color;\n      edge = cssColorToRgb(fill);\n      const fontSize = Number.parseFloat(cs.fontSize) || 16;\n\n      const ta = cs.textAlign;\n      align = normalizeAlign(ta);\n\n      const lh = cs.lineHeight;\n      if (lh === \"normal\") {\n        lineH = 0;\n      } else {\n        const v = Number.parseFloat(lh);\n        lineH = lh.includes(\"px\") ? v : v * fontSize;\n      }\n\n      if (mctx) {\n        mctx.font = font;\n        let widest = 0;\n        for (const t of texts) {\n          for (const line of t.split(\"\\n\")) {\n            widest = Math.max(widest, mctx.measureText(line).width);\n          }\n        }\n        sizer.style.minWidth = `${Math.ceil(widest)}px`;\n      }\n\n      W = host.clientWidth || 1;\n      H = host.clientHeight || 1;\n      padY = Math.ceil((2.6 * maxBlur) / dpr + fontSize * 0.35 + H * 0.02);\n      padX = Math.ceil((2.6 * maxBlur) / dpr + fontSize * 0.35 + W * 0.03);\n      quadW = W + padX * 2;\n      quadH = H + padY * 2;\n      Object.assign(gl.canvas.style, {\n        left: `${-padX}px`,\n        top: `${-padY}px`,\n        width: `${quadW}px`,\n        height: `${quadH}px`,\n      });\n      gl.resize(quadW, quadH, dpr);\n    };\n\n    let index = 0;\n    let seed = 1.7;\n\n    const mount = () => {\n      const art = renderTextTexture({\n        text: texts[index],\n        font,\n        fill,\n        align,\n        lineHeight: lineH,\n        boxW: W,\n        boxH: H,\n        padX,\n        padY,\n        dpr,\n      });\n      if (art) {\n        gl.setTexture(art);\n      }\n    };\n    measure();\n    mount();\n\n    let pTgtX = 0.5;\n    let pTgtY = 0.5;\n    let pCurX = 0.5;\n    let pCurY = 0.5;\n    let cursorUV: [number, number] = [-1, -1];\n    let hoverTgt = 0;\n    let hoverCur = 0;\n    const onMove = (e: PointerEvent) => {\n      const b = host.getBoundingClientRect();\n      const ux = (e.clientX - (b.left - padX)) / (b.width + padX * 2);\n      const uy = (e.clientY - (b.top - padY)) / (b.height + padY * 2);\n      pTgtX = ux;\n      pTgtY = uy;\n      cursorUV = [ux, uy];\n      hoverTgt = 1;\n    };\n    const onLeave = () => {\n      pTgtX = 0.5;\n      pTgtY = 0.5;\n      hoverTgt = 0;\n    };\n    host.addEventListener(\"pointermove\", onMove);\n    host.addEventListener(\"pointerleave\", onLeave);\n\n    let phase: \"in\" | \"hold\" | \"out\" = \"in\";\n    let phaseStart = 0;\n    let progress = 0;\n    let vel = 0;\n    let target = 1;\n    let clock = 0;\n    let last = 0;\n    let raf = 0;\n    let running = false;\n    let held = 0;\n\n    const isLast = () => !loop && index >= texts.length - 1;\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: single rAF frame that advances the spring, phase machine, parallax and per-frame draw of the ported reveal shader.\n    const loopFrame = () => {\n      if (!running) {\n        return;\n      }\n      const now = performance.now();\n      const dt = Math.min(0.05, Math.max(0.001, (now - last) / 1000));\n      last = now;\n      clock += dt;\n\n      const k = target > 0.5 ? K_IN : K_OUT;\n      [progress, vel] = springStep(progress, vel, target, k, DAMP, dt);\n\n      if (phase === \"in\") {\n        if (progress >= REVEALED_AT) {\n          phase = \"hold\";\n          phaseStart = now;\n        }\n      } else if (phase === \"hold\") {\n        if (!isLast() && now - phaseStart >= holdDuration) {\n          phase = \"out\";\n          phaseStart = now;\n          target = 0;\n        }\n      } else if (progress <= GONE_AT && now - phaseStart >= OUT_HOLD_MS) {\n        index = (index + 1) % texts.length;\n        seed = ((seed * 1.618) % 7) + 0.3;\n        mount();\n        phase = \"in\";\n        phaseStart = now;\n        progress = 0;\n        vel = 0;\n        target = 1;\n      }\n\n      const pk = 1 - 0.0009 ** dt;\n      pCurX += (pTgtX - pCurX) * pk;\n      pCurY += (pTgtY - pCurY) * pk;\n      hoverCur += (hoverTgt - hoverCur) * (1 - 0.002 ** dt);\n\n      const amp = PARALLAX_AMP * hoverCur;\n      const parTL: [number, number] = [\n        (pCurX - TL_ANCHOR[0]) * amp,\n        (pCurY - TL_ANCHOR[1]) * amp,\n      ];\n      const parBR: [number, number] = [\n        (pCurX - BR_ANCHOR[0]) * amp,\n        (pCurY - BR_ANCHOR[1]) * amp,\n      ];\n\n      const p = Math.max(0, Math.min(1, progress));\n      const reverse = phase === \"out\" ? 1 : 0;\n      gl.draw(\n        p,\n        maxBlur,\n        edge,\n        clock,\n        quadW / Math.max(1, quadH),\n        seed,\n        parTL,\n        parBR,\n        cursorUV,\n        hoverCur,\n        reverse\n      );\n\n      held += ((phase === \"hold\" ? 1 : 0) - held) * (1 - 0.02 ** dt);\n      const breathe = Math.sin(clock * 0.45) * 0.5 + 0.5;\n      const s = 1 + held * breathe * 0.0015;\n      const b = 1 + held * (breathe - 0.5) * 0.012;\n      gl.canvas.style.transform = `scale(${s.toFixed(4)})`;\n      gl.canvas.style.filter = `brightness(${b.toFixed(3)})`;\n\n      raf = requestAnimationFrame(loopFrame);\n    };\n\n    const start = () => {\n      if (running) {\n        return;\n      }\n      running = true;\n      phase = \"in\";\n      phaseStart = performance.now();\n      last = phaseStart;\n      progress = 0;\n      vel = 0;\n      target = 1;\n      raf = requestAnimationFrame(loopFrame);\n    };\n    const stop = () => {\n      running = false;\n      if (raf) {\n        cancelAnimationFrame(raf);\n      }\n      raf = 0;\n      last = 0;\n    };\n\n    let onScreen = !startOnView;\n    let hidden = false;\n    const sync = () => {\n      if (onScreen && !hidden) {\n        start();\n      } else {\n        stop();\n      }\n    };\n\n    let io: IntersectionObserver | null = null;\n    if (startOnView) {\n      io = new IntersectionObserver(\n        (es) => {\n          onScreen = es[0]?.isIntersecting ?? false;\n          sync();\n        },\n        { threshold: 0.15 }\n      );\n      io.observe(host);\n    } else {\n      sync();\n    }\n\n    const onVis = () => {\n      hidden = document.hidden;\n      sync();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    let resizeT = 0;\n    const ro = new ResizeObserver(() => {\n      window.clearTimeout(resizeT);\n      resizeT = window.setTimeout(() => {\n        if (disposed || host.clientWidth < 2) {\n          return;\n        }\n        measure();\n        mount();\n      }, 120);\n    });\n    ro.observe(host);\n\n    document.fonts?.ready.then(() => {\n      if (disposed) {\n        return;\n      }\n      measure();\n      mount();\n    });\n\n    const themeObs = new MutationObserver(() => {\n      if (disposed) {\n        return;\n      }\n      if (getComputedStyle(host).color !== fill) {\n        measure();\n        mount();\n      }\n    });\n    const themeOpts = {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    };\n    themeObs.observe(document.documentElement, themeOpts);\n    if (document.body) {\n      themeObs.observe(document.body, themeOpts);\n    }\n\n    return () => {\n      disposed = true;\n      stop();\n      io?.disconnect();\n      ro.disconnect();\n      themeObs.disconnect();\n      window.clearTimeout(resizeT);\n      document.removeEventListener(\"visibilitychange\", onVis);\n      host.removeEventListener(\"pointermove\", onMove);\n      host.removeEventListener(\"pointerleave\", onLeave);\n      gl.canvas.remove();\n      gl.destroy();\n    };\n  }, [texts, loop, startOnView, holdDuration, maxBlur, reduced]);\n\n  const sizerText = useMemo(() => {\n    let best = texts[0] ?? \"\";\n    let bestLines = best.split(\"\\n\").length;\n    for (const t of texts) {\n      const n = t.split(\"\\n\").length;\n      if (n > bestLines) {\n        best = t;\n        bestLines = n;\n      }\n    }\n    return best;\n  }, [texts]);\n\n  const setRef = (node: HTMLElement | null) => {\n    hostRef.current = node;\n    if (typeof ref === \"function\") {\n      ref(node);\n    } else if (ref) {\n      ref.current = node;\n    }\n  };\n\n  if (reduced || fallback) {\n    return (\n      <Component\n        className={cn(\"inline-block whitespace-pre\", className)}\n        ref={setRef}\n        {...props}\n      >\n        {texts[0]}\n      </Component>\n    );\n  }\n\n  return (\n    <Component\n      className={cn(\n        \"relative inline-block select-none whitespace-pre align-baseline\",\n        className\n      )}\n      ref={setRef}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"invisible inline-block whitespace-pre\"\n        ref={sizerRef}\n      >\n        {sizerText}\n      </span>\n      <span className=\"sr-only\">{texts.join(\". \")}</span>\n    </Component>\n  );\n}\n\nexport { FogTextReveal };\n",
      "type": "registry:ui",
      "target": "components/sora-ui/texts/fog-text-reveal.tsx"
    }
  ],
  "type": "registry:ui"
}
