{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "WarpText-JS-TW",
	"title": "WarpText",
	"description": "WebGL warp that bends and refracts the text around the pointer.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "WarpText/WarpText.jsx",
			"content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\n\nconst vertex = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D uTextTexture;\nuniform vec2 uResolution;\nuniform vec2 uPointer;\nuniform float uPointerActive;\nuniform float uTime;\nuniform float uWarpStrength;\nuniform float uWarpScale;\nuniform float uSpeed;\nuniform float uPointerInfluence;\nuniform float uPointerStrength;\nuniform float uRefraction;\nuniform float uRipple;\nuniform float uMotion;\n\nin vec2 vUv;\nout vec4 fragColor;\n\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}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 4; i++) {\n    value += amplitude * noise(p);\n    p *= 2.02;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\nvec4 sampleText(vec2 uv) {\n  if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {\n    return vec4(0.0);\n  }\n  return texture(uTextTexture, uv);\n}\n\nvoid main() {\n  vec2 uv = vUv;\n  float aspect = uResolution.x / max(uResolution.y, 1.0);\n  float time = uTime * uSpeed;\n  float scale = max(uWarpScale, 0.001);\n\n  vec2 drift = vec2(time * 0.055, -time * 0.045);\n  float n1 = fbm(uv * scale * 3.1 + drift);\n  float n2 = fbm((uv + 19.17) * scale * 3.4 - drift.yx);\n  vec2 ambient = (vec2(n1, n2) - 0.5) * uWarpStrength * 0.045 * uMotion;\n\n  vec2 pointerDelta = uv - uPointer;\n  vec2 aspectDelta = vec2(pointerDelta.x * aspect, pointerDelta.y);\n  float dist = length(aspectDelta);\n  float radius = max(uPointerInfluence, 0.001);\n  float t = clamp(dist / radius, 0.0, 1.0);\n  float lens = smoothstep(radius, 0.0, dist) * uPointerActive;\n  float bulge = t * (1.0 - t) * (1.0 - t) * 6.75 * uPointerActive;\n  vec2 dir = dist > 0.0001 ? vec2(aspectDelta.x / aspect, aspectDelta.y) / dist : vec2(0.0);\n\n  float rippleWave = sin(dist * 28.0 - time * 4.2) * 0.5 + 0.5;\n  float rippleRing = (rippleWave - 0.5) * uRipple;\n  vec2 pointerWarp = -dir * bulge * uPointerStrength * 0.045;\n  pointerWarp += dir * rippleRing * bulge * uPointerStrength * 0.016;\n\n  vec2 displaced = uv + ambient + pointerWarp;\n  vec2 splitDir = ambient + pointerWarp;\n  float splitLen = length(splitDir);\n  splitDir = splitLen > 0.00001 ? splitDir / splitLen : vec2(0.7071, 0.7071);\n  vec2 split = splitDir * uRefraction * 0.16 * (0.35 + lens * 1.65);\n\n  vec4 base = sampleText(displaced);\n  float r = sampleText(displaced + split).r;\n  float g = base.g;\n  float b = sampleText(displaced - split).b;\n  float a = max(max(sampleText(displaced + split).a, base.a), sampleText(displaced - split).a);\n\n  vec3 color = vec3(r, g, b) + lens * base.a * 0.055;\n  fragColor = vec4(color, a);\n}\n`;\n\nconst getFontValue = value => (typeof value === 'number' ? `${value}px` : value);\n\nconst measureLine = (ctx, line, letterSpacing) => {\n  const chars = Array.from(line);\n  const textWidth = chars.reduce((width, char) => width + ctx.measureText(char).width, 0);\n  return textWidth + Math.max(0, chars.length - 1) * letterSpacing;\n};\n\nconst drawLine = (ctx, line, x, y, letterSpacing) => {\n  const chars = Array.from(line);\n  let cursor = x - measureLine(ctx, line, letterSpacing) / 2;\n\n  chars.forEach((char, index) => {\n    ctx.fillText(char, cursor, y);\n    cursor += ctx.measureText(char).width + (index === chars.length - 1 ? 0 : letterSpacing);\n  });\n};\n\nconst buildTextCanvas = ({ container, width, height, dpr, props }) => {\n  const canvas = document.createElement('canvas');\n  canvas.width = Math.max(1, Math.floor(width * dpr));\n  canvas.height = Math.max(1, Math.floor(height * dpr));\n\n  const ctx = canvas.getContext('2d');\n  if (!ctx) return canvas;\n\n  const probe = document.createElement('span');\n  probe.textContent = props.text;\n  Object.assign(probe.style, {\n    position: 'absolute',\n    visibility: 'hidden',\n    pointerEvents: 'none',\n    whiteSpace: 'pre',\n    inset: '0 auto auto 0',\n    fontFamily: props.fontFamily,\n    fontSize: getFontValue(props.fontSize),\n    fontWeight: String(props.fontWeight),\n    letterSpacing: getFontValue(props.letterSpacing),\n    lineHeight: typeof props.lineHeight === 'number' ? String(props.lineHeight) : props.lineHeight\n  });\n  container.appendChild(probe);\n  const computed = window.getComputedStyle(probe);\n  let fontSizePx = parseFloat(computed.fontSize) || 96;\n  const fontFamily = computed.fontFamily || 'sans-serif';\n  const fontWeight = computed.fontWeight || String(props.fontWeight);\n  let letterSpacing = computed.letterSpacing === 'normal' ? 0 : parseFloat(computed.letterSpacing) || 0;\n  let lineHeight = parseFloat(computed.lineHeight);\n  if (!Number.isFinite(lineHeight)) {\n    lineHeight = fontSizePx * (typeof props.lineHeight === 'number' ? props.lineHeight : 0.92);\n  }\n  probe.remove();\n\n  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n  ctx.clearRect(0, 0, width, height);\n  ctx.textAlign = 'left';\n  ctx.textBaseline = 'middle';\n  ctx.fillStyle = props.color;\n  ctx.imageSmoothingEnabled = true;\n  ctx.imageSmoothingQuality = 'high';\n\n  const lines = String(props.text || '').split('\\n');\n  const applyFont = () => {\n    ctx.font = `${fontWeight} ${fontSizePx}px ${fontFamily}`;\n  };\n  applyFont();\n\n  const maxWidth = width * 0.86;\n  const maxHeight = height * 0.78;\n  const widest = Math.max(...lines.map(line => measureLine(ctx, line, letterSpacing)), 1);\n  const blockHeight = Math.max(lineHeight * lines.length, 1);\n  const fit = Math.min(1, maxWidth / widest, maxHeight / blockHeight);\n\n  if (fit < 1) {\n    fontSizePx *= fit;\n    letterSpacing *= fit;\n    lineHeight *= fit;\n    applyFont();\n  }\n\n  const startY = height / 2 - (lineHeight * (lines.length - 1)) / 2;\n  lines.forEach((line, index) => drawLine(ctx, line, width / 2, startY + index * lineHeight, letterSpacing));\n\n  return canvas;\n};\n\nconst syncUniforms = (program, props) => {\n  const uniforms = program.uniforms;\n  uniforms.uWarpStrength.value = props.warpStrength;\n  uniforms.uWarpScale.value = props.warpScale;\n  uniforms.uSpeed.value = props.speed;\n  uniforms.uPointerInfluence.value = props.pointerInfluence;\n  uniforms.uPointerStrength.value = props.pointerStrength;\n  uniforms.uRefraction.value = props.refraction;\n  uniforms.uRipple.value = props.ripple ? 1 : 0;\n};\n\nconst WarpText = ({\n  text = 'Bend the moment',\n  color = '#f8f5ff',\n  warpStrength = 0.08,\n  warpScale = 1.7,\n  speed = 0.55,\n  pointerInfluence = 0.42,\n  pointerStrength = 0.38,\n  refraction = 0.018,\n  ripple = true,\n  fontSize = 'clamp(3rem, 10vw, 9rem)',\n  fontWeight = 800,\n  fontFamily = 'inherit',\n  letterSpacing = '-0.06em',\n  lineHeight = 0.9,\n  className = '',\n  style\n}) => {\n  const containerRef = useRef(null);\n  const propsRef = useRef({\n    text,\n    color,\n    fontSize,\n    fontWeight,\n    fontFamily,\n    letterSpacing,\n    lineHeight,\n    warpStrength,\n    warpScale,\n    speed,\n    pointerInfluence,\n    pointerStrength,\n    refraction,\n    ripple\n  });\n  const contextRef = useRef(null);\n\n  useEffect(() => {\n    propsRef.current = {\n      text,\n      color,\n      fontSize,\n      fontWeight,\n      fontFamily,\n      letterSpacing,\n      lineHeight,\n      warpStrength,\n      warpScale,\n      speed,\n      pointerInfluence,\n      pointerStrength,\n      refraction,\n      ripple\n    };\n\n    if (contextRef.current) {\n      syncUniforms(contextRef.current.program, propsRef.current);\n      contextRef.current.rasterize();\n    }\n  }, [\n    text,\n    color,\n    fontSize,\n    fontWeight,\n    fontFamily,\n    letterSpacing,\n    lineHeight,\n    warpStrength,\n    warpScale,\n    speed,\n    pointerInfluence,\n    pointerStrength,\n    refraction,\n    ripple\n  ]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container || typeof window === 'undefined') return undefined;\n\n    let renderer;\n    let gl;\n    let program;\n    let geometry;\n    let mesh;\n    let texture;\n    let resizeObserver;\n    let intersectionObserver;\n    let raf = 0;\n    let disposed = false;\n    let contextLost = false;\n    let visible = true;\n    let pageVisible = !document.hidden;\n    let reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n    let rasterVersion = 0;\n\n    const pointer = { x: 0.5, y: 0.5, tx: 0.5, ty: 0.5, active: 0, activeTarget: 0 };\n    const startTime = performance.now();\n\n    try {\n      renderer = new Renderer({\n        webgl: 2,\n        alpha: true,\n        premultipliedAlpha: false,\n        antialias: true,\n        dpr: Math.min(window.devicePixelRatio || 1, 2)\n      });\n      gl = renderer.gl;\n    } catch (error) {\n      console.warn('WarpText: WebGL could not be initialized.', error);\n      return undefined;\n    }\n\n    gl.clearColor(0, 0, 0, 0);\n    const canvas = gl.canvas;\n    canvas.style.position = 'absolute';\n    canvas.style.inset = '0';\n    canvas.style.width = '100%';\n    canvas.style.height = '100%';\n    canvas.style.display = 'block';\n    canvas.setAttribute('aria-hidden', 'true');\n    container.appendChild(canvas);\n\n    texture = new Texture(gl, {\n      generateMipmaps: false,\n      minFilter: gl.LINEAR,\n      magFilter: gl.LINEAR,\n      wrapS: gl.CLAMP_TO_EDGE,\n      wrapT: gl.CLAMP_TO_EDGE\n    });\n\n    geometry = new Triangle(gl);\n    program = new Program(gl, {\n      vertex,\n      fragment,\n      transparent: true,\n      depthTest: false,\n      depthWrite: false,\n      uniforms: {\n        uTextTexture: { value: texture },\n        uResolution: { value: new Float32Array([1, 1]) },\n        uPointer: { value: new Float32Array([0.5, 0.5]) },\n        uPointerActive: { value: 0 },\n        uTime: { value: 0 },\n        uWarpStrength: { value: propsRef.current.warpStrength },\n        uWarpScale: { value: propsRef.current.warpScale },\n        uSpeed: { value: propsRef.current.speed },\n        uPointerInfluence: { value: propsRef.current.pointerInfluence },\n        uPointerStrength: { value: propsRef.current.pointerStrength },\n        uRefraction: { value: propsRef.current.refraction },\n        uRipple: { value: propsRef.current.ripple ? 1 : 0 },\n        uMotion: { value: reduceMotion ? 0 : 1 }\n      }\n    });\n    mesh = new Mesh(gl, { geometry, program });\n\n    const renderOnce = () => {\n      if (disposed || contextLost) return;\n      renderer.render({ scene: mesh });\n    };\n\n    const rasterize = async () => {\n      const version = ++rasterVersion;\n      if (document.fonts?.ready) {\n        try {\n          await document.fonts.ready;\n        } catch (error) {\n          void error;\n        }\n      }\n      if (disposed || contextLost || version !== rasterVersion) return;\n\n      const rect = container.getBoundingClientRect();\n      if (rect.width <= 0 || rect.height <= 0) return;\n\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const textCanvas = buildTextCanvas({\n        container,\n        width: rect.width,\n        height: rect.height,\n        dpr,\n        props: propsRef.current\n      });\n      texture.image = textCanvas;\n      texture.needsUpdate = true;\n      renderOnce();\n    };\n\n    const resize = () => {\n      if (disposed || contextLost) return;\n      const rect = container.getBoundingClientRect();\n      if (rect.width <= 0 || rect.height <= 0) return;\n\n      renderer.dpr = Math.min(window.devicePixelRatio || 1, 2);\n      renderer.setSize(rect.width, rect.height);\n      program.uniforms.uResolution.value[0] = gl.drawingBufferWidth;\n      program.uniforms.uResolution.value[1] = gl.drawingBufferHeight;\n      rasterize();\n    };\n\n    const onPointerMove = event => {\n      if (event.pointerType === 'touch') return;\n      const rect = canvas.getBoundingClientRect();\n      if (rect.width <= 0 || rect.height <= 0) return;\n      pointer.tx = (event.clientX - rect.left) / rect.width;\n      pointer.ty = 1 - (event.clientY - rect.top) / rect.height;\n      pointer.activeTarget = 1;\n    };\n\n    const onPointerLeave = () => {\n      pointer.activeTarget = 0;\n    };\n\n    const onContextLost = event => {\n      event.preventDefault();\n      contextLost = true;\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const onVisibility = () => {\n      pageVisible = !document.hidden;\n      if (pageVisible && visible && !raf) raf = requestAnimationFrame(loop);\n      if (!pageVisible && raf) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    };\n\n    const mediaQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');\n    const onReducedMotion = event => {\n      reduceMotion = event.matches;\n      program.uniforms.uMotion.value = reduceMotion ? 0 : 1;\n      renderOnce();\n    };\n\n    const loop = now => {\n      if (disposed || contextLost) return;\n\n      const elapsed = (now - startTime) * 0.001;\n      const idleX = 0.5 + Math.sin(elapsed * 0.33) * 0.12;\n      const idleY = 0.5 + Math.cos(elapsed * 0.27) * 0.1;\n      const targetX = pointer.activeTarget > 0 ? pointer.tx : idleX;\n      const targetY = pointer.activeTarget > 0 ? pointer.ty : idleY;\n      const damping = pointer.activeTarget > 0 ? 0.12 : 0.035;\n\n      pointer.x += (targetX - pointer.x) * damping;\n      pointer.y += (targetY - pointer.y) * damping;\n      pointer.active += ((pointer.activeTarget > 0 ? 1 : 0.18) - pointer.active) * 0.06;\n\n      program.uniforms.uPointer.value[0] = pointer.x;\n      program.uniforms.uPointer.value[1] = pointer.y;\n      program.uniforms.uPointerActive.value = reduceMotion ? pointer.active * 0.35 : pointer.active;\n      program.uniforms.uTime.value = reduceMotion ? 0 : elapsed;\n\n      renderOnce();\n      raf = requestAnimationFrame(loop);\n    };\n\n    resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n\n    intersectionObserver = new IntersectionObserver(\n      ([entry]) => {\n        visible = entry.isIntersecting;\n        if (visible && pageVisible && !raf) raf = requestAnimationFrame(loop);\n        if (!visible && raf) {\n          cancelAnimationFrame(raf);\n          raf = 0;\n        }\n      },\n      { threshold: 0 }\n    );\n    intersectionObserver.observe(container);\n\n    canvas.addEventListener('pointermove', onPointerMove);\n    canvas.addEventListener('pointerleave', onPointerLeave);\n    canvas.addEventListener('webglcontextlost', onContextLost, false);\n    document.addEventListener('visibilitychange', onVisibility);\n    mediaQuery?.addEventListener('change', onReducedMotion);\n\n    syncUniforms(program, propsRef.current);\n    contextRef.current = { program, rasterize };\n    resize();\n    raf = requestAnimationFrame(loop);\n\n    return () => {\n      disposed = true;\n      contextRef.current = null;\n      if (raf) cancelAnimationFrame(raf);\n      resizeObserver?.disconnect();\n      intersectionObserver?.disconnect();\n      canvas.removeEventListener('pointermove', onPointerMove);\n      canvas.removeEventListener('pointerleave', onPointerLeave);\n      canvas.removeEventListener('webglcontextlost', onContextLost);\n      document.removeEventListener('visibilitychange', onVisibility);\n      mediaQuery?.removeEventListener('change', onReducedMotion);\n\n      if (!contextLost) {\n        try {\n          if (texture?.texture) gl.deleteTexture(texture.texture);\n          geometry?.remove?.();\n          program?.remove?.();\n          gl.getExtension('WEBGL_lose_context')?.loseContext();\n        } catch (error) {\n          void error;\n        }\n      }\n\n      if (canvas.parentNode === container) container.removeChild(canvas);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={containerRef}\n      className={`relative block min-h-[220px] w-full overflow-hidden isolate ${className}`.trim()}\n      style={style}\n      role=\"img\"\n      aria-label={text}\n    />\n  );\n};\n\nexport default WarpText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}