{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ElasticMesh-TS-CSS",
	"title": "ElasticMesh",
	"description": "Spring-mesh surface that stretches under the pointer and settles back with damped physics.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "ElasticMesh.css",
			"target": "@components/ElasticMesh.css",
			"content": ".elastic-mesh {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  min-height: 0;\n  touch-action: none;\n}\n\n.elastic-mesh canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "ElasticMesh.tsx",
			"content": "import { useEffect, useRef } from 'react';\nimport type { CSSProperties } from 'react';\nimport { Renderer, Geometry, Program, Mesh, Texture } from 'ogl';\n\nimport './ElasticMesh.css';\n\nconst DIST = 4.6;\nconst FIT = 0.82;\n\nconst VERT = `\nprecision highp float;\nattribute vec2 aGrid;\nattribute vec2 uv;\nattribute vec3 aOffset;\nattribute vec3 aNormal;\n\nuniform float uAspect;\nuniform float uTilt;\nuniform float uDist;\nuniform float uFit;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nvoid main() {\n  vUv = uv;\n\n  vec2 base = vec2((aGrid.x * 2.0 - 1.0) * uAspect, 1.0 - aGrid.y * 2.0);\n  vec3 p = vec3(base + aOffset.xy, aOffset.z);\n\n  float ct = cos(uTilt);\n  float st = sin(uTilt);\n  float ry = p.y * ct - p.z * st;\n  float rz = p.y * st + p.z * ct;\n  p.y = ry;\n  p.z = rz;\n\n  float persp = uDist / (uDist - p.z);\n  vec2 clip = vec2(p.x / uAspect, p.y) * persp * uFit;\n\n  vNormal = aNormal;\n  vDepth = aOffset.z;\n  gl_Position = vec4(clip, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `\nprecision highp float;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying float vDepth;\n\nuniform sampler2D tMap;\nuniform float uHasImage;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uHighlight;\nuniform float uShading;\nuniform vec2 uRes;\nuniform float uRadius;\nuniform float uGrid;\nuniform float uGridDensity;\nuniform float uGridOpacity;\nuniform vec3 uGridColor;\n\nvoid main() {\n  vec3 base;\n  if (uHasImage > 0.5) {\n    base = texture2D(tMap, vUv).rgb;\n  } else {\n    base = mix(uColor1, uColor2, clamp(vUv.y, 0.0, 1.0));\n  }\n\n  vec3 N = normalize(vNormal);\n  vec3 L = normalize(vec3(-0.35, 0.55, 0.78));\n  vec3 V = vec3(0.0, 0.0, 1.0);\n  vec3 H = normalize(L + V);\n\n  float diff = clamp(dot(N, L), 0.0, 1.0);\n  float specRaw = pow(clamp(dot(N, H), 0.0, 1.0), 26.0);\n  float specFlat = pow(clamp(H.z, 0.0, 1.0), 26.0);\n  float spec = clamp((specRaw - specFlat) / (1.0 - specFlat), 0.0, 1.0);\n  float ao = clamp(1.0 + vDepth * 0.45, 0.65, 1.25);\n\n  vec3 lit = base * (1.0 - uShading * 0.28);\n  lit += base * diff * uShading * 0.55;\n  lit *= ao;\n  lit += uHighlight * spec * uShading * 0.25;\n\n  if (uGrid > 0.5) {\n    vec2 g = vUv * uGridDensity;\n    vec2 w = uGridDensity / max(uRes, vec2(1.0));\n    vec2 d = abs(fract(g - 0.5) - 0.5) / max(w * 1.5, vec2(1e-4));\n    float line = 1.0 - clamp(min(d.x, d.y), 0.0, 1.0);\n    lit = mix(lit, uGridColor, line * uGridOpacity * (0.45 + diff * 0.55));\n  }\n\n  vec2 p = (vUv - 0.5) * uRes;\n  vec2 halfRes = uRes * 0.5;\n  float r = min(uRadius, min(halfRes.x, halfRes.y));\n  vec2 q = abs(p) - (halfRes - r);\n  float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n  float alpha = 1.0 - smoothstep(-1.25, 1.25, sd);\n  if (alpha <= 0.002) discard;\n\n  gl_FragColor = vec4(lit, alpha);\n}\n`;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = (hex || '').replace('#', '').trim();\n  if (h.length === 3)\n    h = h\n      .split('')\n      .map(c => c + c)\n      .join('');\n  const n = parseInt(h || '000000', 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nexport interface ElasticMeshProps {\n  image?: string;\n  color1?: string;\n  color2?: string;\n  highlight?: string;\n  showGrid?: boolean;\n  gridDensity?: number;\n  gridOpacity?: number;\n  gridColor?: string;\n  borderRadius?: number;\n  stiffness?: number;\n  damping?: number;\n  grabRadius?: number;\n  pull?: number;\n  wobble?: number;\n  tilt?: number;\n  shading?: number;\n  resolution?: number;\n  interaction?: 'hover' | 'drag';\n  enabled?: boolean;\n  className?: string;\n  style?: CSSProperties;\n  [key: string]: unknown;\n}\n\nconst ElasticMesh = ({\n  image = '',\n  color1 = '#5227FF',\n  color2 = '#B19EEF',\n  highlight = '#ffffff',\n  showGrid = true,\n  gridDensity = 20,\n  gridOpacity = 0.28,\n  gridColor = '#ffffff',\n  borderRadius = 25,\n  stiffness = 0.05,\n  damping = 0.2,\n  grabRadius = 0.6,\n  pull = 0.4,\n  wobble = 5,\n  tilt = 14,\n  shading = 0.5,\n  resolution = 25,\n  interaction = 'hover',\n  enabled = true,\n  className = '',\n  style,\n  ...rest\n}: ElasticMeshProps) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n\n  const propsRef = useRef<Record<string, any>>({});\n  propsRef.current = {\n    color1,\n    color2,\n    highlight,\n    showGrid,\n    gridDensity,\n    gridOpacity,\n    gridColor,\n    borderRadius,\n    stiffness,\n    damping,\n    grabRadius,\n    pull,\n    wobble,\n    tilt,\n    shading,\n    interaction,\n    enabled\n  };\n\n  useEffect(() => {\n    const container = containerRef.current as HTMLDivElement;\n    if (!container) return;\n\n    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    const renderer = new Renderer({ alpha: true, antialias: true, dpr: Math.min(window.devicePixelRatio || 1, 2) });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 0);\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n    const N = Math.max(6, Math.min(40, Math.round(resolution)));\n    const nodeCount = N * N;\n\n    const aGrid = new Float32Array(nodeCount * 2);\n    const uv = new Float32Array(nodeCount * 2);\n    const aOffset = new Float32Array(nodeCount * 3);\n    const aNormal = new Float32Array(nodeCount * 3);\n\n    for (let j = 0; j < N; j++) {\n      for (let i = 0; i < N; i++) {\n        const idx = j * N + i;\n        const u = i / (N - 1);\n        const v = j / (N - 1);\n        aGrid[idx * 2] = u;\n        aGrid[idx * 2 + 1] = v;\n        uv[idx * 2] = u;\n        uv[idx * 2 + 1] = v;\n        aNormal[idx * 3 + 2] = 1;\n      }\n    }\n\n    const quads = (N - 1) * (N - 1);\n    const index = new Uint16Array(quads * 6);\n    let t = 0;\n    for (let j = 0; j < N - 1; j++) {\n      for (let i = 0; i < N - 1; i++) {\n        const a = j * N + i;\n        const b = a + 1;\n        const c = a + N;\n        const d = c + 1;\n        index[t++] = a;\n        index[t++] = c;\n        index[t++] = b;\n        index[t++] = b;\n        index[t++] = c;\n        index[t++] = d;\n      }\n    }\n\n    const geometry = new Geometry(gl, {\n      aGrid: { size: 2, data: aGrid },\n      uv: { size: 2, data: uv },\n      aOffset: { size: 3, data: aOffset },\n      aNormal: { size: 3, data: aNormal },\n      index: { data: index }\n    });\n\n    const texture = new Texture(gl, { generateMipmaps: false, flipY: false });\n    let hasImage = 0;\n    if (image) {\n      const img = new Image();\n      img.crossOrigin = 'anonymous';\n      img.src = image;\n      img.onload = () => {\n        texture.image = img;\n        program.uniforms.uHasImage.value = 1;\n      };\n    }\n\n    const program = new Program(gl, {\n      vertex: VERT,\n      fragment: FRAG,\n      transparent: true,\n      cullFace: null,\n      uniforms: {\n        tMap: { value: texture },\n        uHasImage: { value: hasImage },\n        uColor1: { value: hexToRgb(color1) },\n        uColor2: { value: hexToRgb(color2) },\n        uHighlight: { value: hexToRgb(highlight) },\n        uGrid: { value: showGrid ? 1 : 0 },\n        uGridDensity: { value: gridDensity },\n        uGridOpacity: { value: gridOpacity },\n        uGridColor: { value: hexToRgb(gridColor) },\n        uShading: { value: shading },\n        uRes: { value: [1, 1] },\n        uRadius: { value: borderRadius },\n        uAspect: { value: 1 },\n        uTilt: { value: (tilt * Math.PI) / 180 },\n        uDist: { value: DIST },\n        uFit: { value: FIT }\n      }\n    });\n\n    const mesh = new Mesh(gl, { geometry, program });\n\n    const baseX = new Float32Array(nodeCount);\n    const baseY = new Float32Array(nodeCount);\n    const pos = new Float32Array(nodeCount * 3);\n    const vel = new Float32Array(nodeCount * 3);\n    const accel = new Float32Array(nodeCount * 3);\n\n    let aspect = 1;\n    function refreshBase() {\n      for (let idx = 0; idx < nodeCount; idx++) {\n        baseX[idx] = (aGrid[idx * 2] * 2 - 1) * aspect;\n        baseY[idx] = 1 - aGrid[idx * 2 + 1] * 2;\n      }\n    }\n\n    function resize() {\n      const w = container.offsetWidth || 1;\n      const h = container.offsetHeight || 1;\n      renderer.setSize(w, h);\n      aspect = w / h;\n      program.uniforms.uAspect.value = aspect;\n      program.uniforms.uRes.value = [w, h];\n      refreshBase();\n    }\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    const pointer = { x: 0, y: 0, tx: 0, ty: 0, active: false, targetActive: false };\n\n    function toPlane(clientX: number, clientY: number) {\n      const rect = container.getBoundingClientRect();\n      const mx = (clientX - rect.left) / rect.width;\n      const my = (clientY - rect.top) / rect.height;\n      const clipX = mx * 2 - 1;\n      const clipY = 1 - my * 2;\n      const t = ((propsRef.current.tilt || 0) * Math.PI) / 180;\n      const ct = Math.cos(t);\n      const st = Math.sin(t);\n      const a = clipY / (ct * FIT * DIST);\n      const py = (a * DIST) / (1 + a * st);\n      const persp = DIST / (DIST - py * st);\n      pointer.tx = (clipX * aspect) / (persp * FIT);\n      pointer.ty = py;\n    }\n\n    function onMove(e: MouseEvent) {\n      toPlane(e.clientX, e.clientY);\n      if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n    }\n    function onEnter() {\n      if (propsRef.current.interaction === 'hover') pointer.targetActive = true;\n    }\n    function onLeave() {\n      pointer.targetActive = false;\n    }\n    function onDown(e: MouseEvent) {\n      if (propsRef.current.interaction === 'drag') {\n        toPlane(e.clientX, e.clientY);\n        pointer.x = pointer.tx;\n        pointer.y = pointer.ty;\n        pointer.targetActive = true;\n      }\n    }\n    function onUp() {\n      if (propsRef.current.interaction === 'drag') pointer.targetActive = false;\n    }\n    function onTouch(e: TouchEvent) {\n      if (e.touches.length) {\n        toPlane(e.touches[0].clientX, e.touches[0].clientY);\n        pointer.targetActive = true;\n      }\n    }\n\n    container.addEventListener('mousemove', onMove);\n    container.addEventListener('mouseenter', onEnter);\n    container.addEventListener('mouseleave', onLeave);\n    container.addEventListener('mousedown', onDown);\n    window.addEventListener('mouseup', onUp);\n    container.addEventListener('touchstart', onTouch, { passive: true });\n    container.addEventListener('touchmove', onTouch, { passive: true });\n    container.addEventListener('touchend', onLeave);\n\n    const STEP = 1 / 120;\n    const MAX_SUB = 5;\n    let accTime = 0;\n    let last = performance.now();\n    let maxOffset = 0;\n    let maxVel = 0;\n\n    function substep() {\n      const p = propsRef.current;\n      const s = p.stiffness;\n      const retain = 1 - p.damping;\n      const coupling = 0.06 + p.wobble * 0.032;\n      const active = pointer.active && p.enabled && !reduceMotion;\n      const r = Math.max(0.08, p.grabRadius) * 1.4;\n      const invR = 1 / r;\n      const force = p.pull * 0.009;\n\n      for (let j = 0; j < N; j++) {\n        for (let i = 0; i < N; i++) {\n          const idx = j * N + i;\n          const o3 = idx * 3;\n          const ox = pos[o3];\n          const oy = pos[o3 + 1];\n          const oz = pos[o3 + 2];\n\n          let ax = -s * ox;\n          let ay = -s * oy;\n          let az = -s * oz;\n\n          let sumx = 0;\n          let sumy = 0;\n          let sumz = 0;\n          let cnt = 0;\n          if (i > 0) {\n            const n = (idx - 1) * 3;\n            sumx += pos[n];\n            sumy += pos[n + 1];\n            sumz += pos[n + 2];\n            cnt++;\n          }\n          if (i < N - 1) {\n            const n = (idx + 1) * 3;\n            sumx += pos[n];\n            sumy += pos[n + 1];\n            sumz += pos[n + 2];\n            cnt++;\n          }\n          if (j > 0) {\n            const n = (idx - N) * 3;\n            sumx += pos[n];\n            sumy += pos[n + 1];\n            sumz += pos[n + 2];\n            cnt++;\n          }\n          if (j < N - 1) {\n            const n = (idx + N) * 3;\n            sumx += pos[n];\n            sumy += pos[n + 1];\n            sumz += pos[n + 2];\n            cnt++;\n          }\n          ax += coupling * (sumx - cnt * ox);\n          ay += coupling * (sumy - cnt * oy);\n          az += coupling * (sumz - cnt * oz);\n\n          if (active) {\n            const dx = pointer.x - (baseX[idx] + ox);\n            const dy = pointer.y - (baseY[idx] + oy);\n            const d = Math.sqrt(dx * dx + dy * dy);\n            const tnorm = d * invR;\n            if (tnorm < 1) {\n              const zBump = 1 - tnorm * tnorm;\n              az += force * zBump * zBump * 6.0;\n              if (d > 1e-4) {\n                const pinch = tnorm * (1 - tnorm) * (1 - tnorm) * 6.75;\n                const dir = (force * pinch * 1.6) / d;\n                ax += dx * dir;\n                ay += dy * dir;\n              }\n            }\n          }\n\n          accel[o3] = ax;\n          accel[o3 + 1] = ay;\n          accel[o3 + 2] = az;\n        }\n      }\n\n      for (let k = 0; k < nodeCount; k++) {\n        const o3 = k * 3;\n        const nvx = (vel[o3] + accel[o3]) * retain;\n        const nvy = (vel[o3 + 1] + accel[o3 + 1]) * retain;\n        const nvz = (vel[o3 + 2] + accel[o3 + 2]) * retain;\n        vel[o3] = nvx;\n        vel[o3 + 1] = nvy;\n        vel[o3 + 2] = nvz;\n\n        let px = pos[o3] + nvx;\n        let py = pos[o3 + 1] + nvy;\n        let pz = pos[o3 + 2] + nvz;\n        if (px > 1.2) px = 1.2;\n        else if (px < -1.2) px = -1.2;\n        if (py > 1.2) py = 1.2;\n        else if (py < -1.2) py = -1.2;\n        if (pz > 1.2) pz = 1.2;\n        else if (pz < -1.2) pz = -1.2;\n        pos[o3] = px;\n        pos[o3 + 1] = py;\n        pos[o3 + 2] = pz;\n      }\n    }\n\n    function commit() {\n      maxOffset = 0;\n      maxVel = 0;\n      for (let j = 0; j < N; j++) {\n        for (let i = 0; i < N; i++) {\n          const idx = j * N + i;\n          const o3 = idx * 3;\n          const iL = i > 0 ? idx - 1 : idx;\n          const iR = i < N - 1 ? idx + 1 : idx;\n          const iD = j > 0 ? idx - N : idx;\n          const iU = j < N - 1 ? idx + N : idx;\n\n          const lx = baseX[iL] + pos[iL * 3];\n          const ly = baseY[iL] + pos[iL * 3 + 1];\n          const lz = pos[iL * 3 + 2];\n          const rx = baseX[iR] + pos[iR * 3];\n          const ry = baseY[iR] + pos[iR * 3 + 1];\n          const rz = pos[iR * 3 + 2];\n          const dx = baseX[iD] + pos[iD * 3];\n          const dy = baseY[iD] + pos[iD * 3 + 1];\n          const dz = pos[iD * 3 + 2];\n          const ux = baseX[iU] + pos[iU * 3];\n          const uy = baseY[iU] + pos[iU * 3 + 1];\n          const uz = pos[iU * 3 + 2];\n\n          const txx = rx - lx;\n          const txy = ry - ly;\n          const txz = rz - lz;\n          const tyx = ux - dx;\n          const tyy = uy - dy;\n          const tyz = uz - dz;\n\n          let nx = txy * tyz - txz * tyy;\n          let ny = txz * tyx - txx * tyz;\n          let nz = txx * tyy - txy * tyx;\n          if (nz < 0) {\n            nx = -nx;\n            ny = -ny;\n            nz = -nz;\n          }\n          const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;\n          aNormal[o3] = nx / len;\n          aNormal[o3 + 1] = ny / len;\n          aNormal[o3 + 2] = nz / len;\n\n          aOffset[o3] = pos[o3];\n          aOffset[o3 + 1] = pos[o3 + 1];\n          aOffset[o3 + 2] = pos[o3 + 2];\n\n          const om = Math.abs(pos[o3]) + Math.abs(pos[o3 + 1]) + Math.abs(pos[o3 + 2]);\n          if (om > maxOffset) maxOffset = om;\n          const vm = Math.abs(vel[o3]) + Math.abs(vel[o3 + 1]) + Math.abs(vel[o3 + 2]);\n          if (vm > maxVel) maxVel = vm;\n        }\n      }\n      geometry.attributes.aOffset.needsUpdate = true;\n      geometry.attributes.aNormal.needsUpdate = true;\n    }\n\n    let raf = 0;\n    function frame(now: number) {\n      raf = requestAnimationFrame(frame);\n      const p = propsRef.current;\n\n      program.uniforms.uShading.value = p.shading;\n      program.uniforms.uRadius.value = p.borderRadius;\n      program.uniforms.uTilt.value = (p.tilt * Math.PI) / 180;\n      program.uniforms.uColor1.value = hexToRgb(p.color1);\n      program.uniforms.uColor2.value = hexToRgb(p.color2);\n      program.uniforms.uHighlight.value = hexToRgb(p.highlight);\n      program.uniforms.uGrid.value = p.showGrid ? 1 : 0;\n      program.uniforms.uGridDensity.value = p.gridDensity;\n      program.uniforms.uGridOpacity.value = p.gridOpacity;\n      program.uniforms.uGridColor.value = hexToRgb(p.gridColor);\n\n      let dt = (now - last) / 1000;\n      last = now;\n      if (dt > 0.25) dt = 0.25;\n\n      const tau = 0.06;\n      const kLerp = 1 - Math.exp(-Math.max(dt, 1e-4) / tau);\n      pointer.x += (pointer.tx - pointer.x) * kLerp;\n      pointer.y += (pointer.ty - pointer.y) * kLerp;\n      pointer.active = pointer.targetActive;\n\n      accTime += dt;\n      let sub = 0;\n      while (accTime >= STEP && sub < MAX_SUB) {\n        substep();\n        accTime -= STEP;\n        sub++;\n      }\n      if (accTime > STEP) accTime = 0;\n\n      commit();\n      renderer.render({ scene: mesh });\n    }\n    raf = requestAnimationFrame(frame);\n\n    container.appendChild(gl.canvas);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      container.removeEventListener('mousemove', onMove);\n      container.removeEventListener('mouseenter', onEnter);\n      container.removeEventListener('mouseleave', onLeave);\n      container.removeEventListener('mousedown', onDown);\n      window.removeEventListener('mouseup', onUp);\n      container.removeEventListener('touchstart', onTouch);\n      container.removeEventListener('touchmove', onTouch);\n      container.removeEventListener('touchend', onLeave);\n      if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n      const lose = gl.getExtension('WEBGL_lose_context');\n      if (lose) lose.loseContext();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [image, resolution]);\n\n  return (\n    <div ref={containerRef} className={`elastic-mesh${className ? ` ${className}` : ''}`} style={style} {...rest} />\n  );\n};\n\nexport default ElasticMesh;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}