{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GlowCursor-TS-CSS",
	"title": "GlowCursor",
	"description": "Shader-powered light trail that smoothly follows the pointer with customizable glow, color, taper and pulse.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "GlowCursor.css",
			"target": "@components/GlowCursor.css",
			"content": ".glow-cursor {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.glow-cursor__canvas {\n  position: absolute;\n  inset: 0;\n  display: block;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  user-select: none;\n}\n\n.glow-cursor__content {\n  position: relative;\n  z-index: 1;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "GlowCursor.tsx",
			"content": "import { useEffect, useRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { Mesh, Program, Renderer, Triangle } from 'ogl';\nimport './GlowCursor.css';\n\nconst MAX_POINTS = 64;\n\ntype BlendMode = 'normal' | 'screen' | 'plus-lighter';\n\nexport interface GlowCursorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {\n  color?: string;\n  secondaryColor?: string;\n  trailLength?: number;\n  trailWidth?: number;\n  trailTaper?: number;\n  followSpeed?: number;\n  glowIntensity?: number;\n  glowSpread?: number;\n  hotspot?: number;\n  brightness?: number;\n  opacity?: number;\n  pulseSpeed?: number;\n  noiseStrength?: number;\n  idleFade?: boolean;\n  idleTimeout?: number;\n  fadeDuration?: number;\n  blendMode?: BlendMode;\n  maxDevicePixelRatio?: number;\n  enabled?: boolean;\n  children?: ReactNode;\n}\n\ninterface GlowCursorConfig {\n  color: string;\n  secondaryColor: string;\n  trailLength: number;\n  trailWidth: number;\n  trailTaper: number;\n  followSpeed: number;\n  glowIntensity: number;\n  glowSpread: number;\n  hotspot: number;\n  brightness: number;\n  opacity: number;\n  pulseSpeed: number;\n  noiseStrength: number;\n  idleFade: boolean;\n  idleTimeout: number;\n  fadeDuration: number;\n  blendMode: BlendMode;\n  maxDevicePixelRatio: number;\n  enabled: boolean;\n}\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\n\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\n\n#define MAX_POINTS 64\n\nuniform vec2 uResolution;\nuniform vec2 uPoints[MAX_POINTS];\nuniform float uPointCount;\nuniform vec3 uColor;\nuniform vec3 uSecondaryColor;\nuniform float uTrailWidth;\nuniform float uTaper;\nuniform float uGlowIntensity;\nuniform float uGlowSpread;\nuniform float uHotspot;\nuniform float uBrightness;\nuniform float uOpacity;\nuniform float uPulseSpeed;\nuniform float uNoiseStrength;\nuniform float uNormalBlend;\nuniform float uTime;\nuniform float uFade;\n\nvarying vec2 vUv;\n\nfloat sRGB(float x) {\n  if (x <= 0.00031308) return 12.92 * x;\n  return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat filmGrain(vec2 p, float time) {\n  float frame = time * 18.0;\n  float frameIndex = mod(floor(frame), 256.0);\n  float nextFrameIndex = mod(frameIndex + 1.0, 256.0);\n  float blend = fract(frame);\n  blend = blend * blend * (3.0 - 2.0 * blend);\n  vec2 pixel = floor(p);\n  float current = hash(pixel + vec2(frameIndex * 17.0, frameIndex * 31.0));\n  float next = hash(pixel + vec2(nextFrameIndex * 17.0, nextFrameIndex * 31.0));\n  return mix(current, next, blend) * 2.0 - 1.0;\n}\n\nvoid main() {\n  vec2 pixel = vUv * uResolution;\n  float denominator = max(uPointCount - 1.0, 1.0);\n  float strongest = 0.0;\n  float strongestCore = 0.0;\n  float colorWeight = 0.0;\n  vec3 colorSum = vec3(0.0);\n\n  for (int i = 0; i < MAX_POINTS - 1; i++) {\n    float index = float(i);\n    float active = 1.0 - step(uPointCount - 1.0, index);\n    vec2 start = uPoints[i];\n    vec2 end = uPoints[i + 1];\n    vec2 toPixel = pixel - start;\n    vec2 segment = end - start;\n    float along = clamp(dot(toPixel, segment) / max(dot(segment, segment), 0.0001), 0.0, 1.0);\n    float progress = clamp((index + along) / denominator, 0.0, 1.0);\n    float life = pow(max(1.0 - progress, 0.0), mix(0.55, 1.25, uTaper));\n    float width = uTrailWidth * mix(1.0, 0.25, pow(progress, mix(0.55, 1.6, uTaper)));\n    float distanceToTrail = length(toPixel - segment * along);\n    float falloff = max(width * (0.8 + uGlowSpread * 1.4), 0.5);\n    float beam = min(1.0, (falloff * falloff) / (distanceToTrail * distanceToTrail + falloff * falloff));\n    float core = exp(-pow(distanceToTrail / max(width, 0.5), 2.0) * 2.5);\n    float pulseAmount = min(abs(uPulseSpeed), 1.0);\n    float pulse = 1.0 + sin(uTime * uPulseSpeed * 3.0 - progress * 11.0) * 0.16 * pulseAmount;\n    float intensity = (core + beam * uGlowIntensity * 0.55) * life * pulse * active;\n    vec3 segmentColor = mix(uColor, uSecondaryColor, progress);\n\n    strongest = max(strongest, intensity);\n    strongestCore = max(strongestCore, core * life * active);\n    colorSum += segmentColor * intensity;\n    colorWeight += intensity;\n  }\n\n  float grain = filmGrain(pixel, uTime);\n  float noiseAmount = (1.0 - exp(-uNoiseStrength * 2.2)) * 0.4;\n  float alpha = clamp(strongest * uOpacity * uFade, 0.0, 1.0);\n  if (alpha < 0.0005) discard;\n\n  vec3 color = colorSum / max(colorWeight, 0.0001);\n  color = mix(color, vec3(1.0), smoothstep(0.25, 0.95, strongestCore) * uHotspot);\n  float luminance = sRGB(clamp(strongest * uBrightness, 0.0, 1.0));\n  luminance *= 1.0 + grain * noiseAmount;\n  vec3 additiveColor = color * luminance;\n  float normalAlpha = clamp(strongest * uBrightness * uOpacity * uFade, 0.0, 1.0);\n  vec3 normalColor = mix(color, vec3(1.0), smoothstep(0.45, 1.0, strongestCore) * uHotspot * 0.35);\n  gl_FragColor = vec4(mix(additiveColor, normalColor, uNormalBlend), mix(alpha, normalAlpha, uNormalBlend));\n}\n`;\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  let value = (hex || '').replace('#', '').trim();\n  if (value.length === 3)\n    value = value\n      .split('')\n      .map((char: string) => char + char)\n      .join('');\n  const parsed = Number.parseInt(value || '000000', 16);\n  return [((parsed >> 16) & 255) / 255, ((parsed >> 8) & 255) / 255, (parsed & 255) / 255];\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nconst GlowCursor = ({\n  color = '#67E8F9',\n  secondaryColor = '#A78BFA',\n  trailLength = 40,\n  trailWidth = 8,\n  trailTaper = 0.8,\n  followSpeed = 0.16,\n  glowIntensity = 1.9,\n  glowSpread = 1.2,\n  hotspot = 0.65,\n  brightness = 1.25,\n  opacity = 1,\n  pulseSpeed = 1.1,\n  noiseStrength = 0.035,\n  idleFade = true,\n  idleTimeout = 700,\n  fadeDuration = 900,\n  blendMode = 'screen',\n  maxDevicePixelRatio = 1.5,\n  enabled = true,\n  children,\n  className = '',\n  style,\n  ...rest\n}: GlowCursorProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const propsRef = useRef<GlowCursorConfig>({} as GlowCursorConfig);\n\n  propsRef.current = {\n    color,\n    secondaryColor,\n    trailLength,\n    trailWidth,\n    trailTaper,\n    followSpeed,\n    glowIntensity,\n    glowSpread,\n    hotspot,\n    brightness,\n    opacity,\n    pulseSpeed,\n    noiseStrength,\n    idleFade,\n    idleTimeout,\n    fadeDuration,\n    maxDevicePixelRatio,\n    blendMode,\n    enabled\n  };\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) return;\n\n    const initialConfig = propsRef.current;\n    const renderer = new Renderer({\n      canvas,\n      alpha: true,\n      dpr: Math.min(window.devicePixelRatio || 1, initialConfig.maxDevicePixelRatio)\n    });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 0);\n\n    const pointData = Array(MAX_POINTS * 2).fill(0);\n    const points = Array.from({ length: MAX_POINTS }, () => ({ x: 0, y: 0 }));\n    const target = { x: 0, y: 0 };\n    const head = { x: 0, y: 0 };\n\n    const program = new Program(gl, {\n      vertex: VERTEX_SHADER,\n      fragment: FRAGMENT_SHADER,\n      uniforms: {\n        uResolution: { value: [1, 1] },\n        uPoints: { value: pointData },\n        uPointCount: { value: initialConfig.trailLength },\n        uColor: { value: hexToRgb(initialConfig.color) },\n        uSecondaryColor: { value: hexToRgb(initialConfig.secondaryColor) },\n        uTrailWidth: { value: initialConfig.trailWidth },\n        uTaper: { value: initialConfig.trailTaper },\n        uGlowIntensity: { value: initialConfig.glowIntensity },\n        uGlowSpread: { value: initialConfig.glowSpread },\n        uHotspot: { value: initialConfig.hotspot },\n        uBrightness: { value: initialConfig.brightness },\n        uOpacity: { value: initialConfig.opacity },\n        uPulseSpeed: { value: initialConfig.pulseSpeed },\n        uNoiseStrength: { value: initialConfig.noiseStrength },\n        uNormalBlend: { value: initialConfig.blendMode === 'normal' ? 1 : 0 },\n        uTime: { value: 0 },\n        uFade: { value: 0 }\n      },\n      transparent: true,\n      depthTest: false,\n      depthWrite: false\n    });\n    const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });\n\n    let width = 1;\n    let height = 1;\n    let initialized = false;\n    let pointerInside = false;\n    let fade = 0;\n    let lastInputTime = performance.now();\n    let lastFrameTime = performance.now();\n    let raf = 0;\n    let destroyed = false;\n\n    const resize = () => {\n      width = Math.max(container.clientWidth, 1);\n      height = Math.max(container.clientHeight, 1);\n      renderer.setSize(width, height);\n      program.uniforms.uResolution.value = [width, height];\n    };\n\n    const initializeTrail = (x: number, y: number) => {\n      target.x = x;\n      target.y = y;\n      head.x = x;\n      head.y = y;\n      for (const point of points) {\n        point.x = x;\n        point.y = y;\n      }\n      initialized = true;\n      fade = 1;\n    };\n\n    const updatePointer = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const x = clamp(event.clientX - rect.left, 0, rect.width);\n      const y = clamp(rect.height - (event.clientY - rect.top), 0, rect.height);\n      if (!initialized) initializeTrail(x, y);\n      target.x = x;\n      target.y = y;\n      pointerInside = true;\n      lastInputTime = performance.now();\n    };\n\n    const onPointerLeave = () => {\n      pointerInside = false;\n      lastInputTime = performance.now();\n    };\n\n    const render = (now: number) => {\n      if (destroyed) return;\n      const config = propsRef.current;\n      const delta = Math.min((now - lastFrameTime) / 16.667, 3);\n      lastFrameTime = now;\n\n      if (initialized) {\n        const headEase = 1 - Math.pow(1 - clamp(config.followSpeed, 0.01, 0.99), delta);\n        const chainBase = clamp(0.28 + config.followSpeed * 0.35, 0.08, 0.92);\n        const chainEase = 1 - Math.pow(1 - chainBase, delta);\n        head.x += (target.x - head.x) * headEase;\n        head.y += (target.y - head.y) * headEase;\n        points[0].x = head.x;\n        points[0].y = head.y;\n\n        for (let i = 1; i < MAX_POINTS; i++) {\n          points[i].x += (points[i - 1].x - points[i].x) * chainEase;\n          points[i].y += (points[i - 1].y - points[i].y) * chainEase;\n        }\n\n        for (let i = 0; i < MAX_POINTS; i++) {\n          pointData[i * 2] = points[i].x;\n          pointData[i * 2 + 1] = points[i].y;\n        }\n      }\n\n      const idleFor = now - lastInputTime;\n      const shouldFade = config.idleFade && (!pointerInside || idleFor > config.idleTimeout);\n      const fadeStep = (16.667 * delta) / Math.max(config.fadeDuration, 16);\n      const fadeTarget = initialized && config.enabled && !shouldFade ? 1 : 0;\n      fade += (fadeTarget - fade) * Math.min(1, fadeStep * 7);\n\n      program.uniforms.uPointCount.value = clamp(Math.round(config.trailLength), 2, MAX_POINTS);\n      program.uniforms.uColor.value = hexToRgb(config.color);\n      program.uniforms.uSecondaryColor.value = hexToRgb(config.secondaryColor);\n      program.uniforms.uTrailWidth.value = Math.max(config.trailWidth, 0.1);\n      program.uniforms.uTaper.value = clamp(config.trailTaper, 0, 1);\n      program.uniforms.uGlowIntensity.value = Math.max(config.glowIntensity, 0);\n      program.uniforms.uGlowSpread.value = Math.max(config.glowSpread, 0);\n      program.uniforms.uHotspot.value = clamp(config.hotspot, 0, 1);\n      program.uniforms.uBrightness.value = Math.max(config.brightness, 0);\n      program.uniforms.uOpacity.value = clamp(config.opacity, 0, 1);\n      program.uniforms.uPulseSpeed.value = config.pulseSpeed;\n      program.uniforms.uNoiseStrength.value = clamp(config.noiseStrength, 0, 1);\n      program.uniforms.uNormalBlend.value = config.blendMode === 'normal' ? 1 : 0;\n      program.uniforms.uTime.value = now * 0.001;\n      program.uniforms.uFade.value = fade;\n\n      renderer.render({ scene: mesh });\n      if (!destroyed) raf = requestAnimationFrame(render);\n    };\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    container.addEventListener('pointermove', updatePointer);\n    container.addEventListener('pointerenter', updatePointer);\n    container.addEventListener('pointerleave', onPointerLeave);\n    resize();\n    raf = requestAnimationFrame(render);\n\n    return () => {\n      destroyed = true;\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      container.removeEventListener('pointermove', updatePointer);\n      container.removeEventListener('pointerenter', updatePointer);\n      container.removeEventListener('pointerleave', onPointerLeave);\n      mesh.geometry.remove();\n      program.remove();\n    };\n  }, [maxDevicePixelRatio]);\n\n  return (\n    <div ref={containerRef} className={`glow-cursor${className ? ` ${className}` : ''}`} style={style} {...rest}>\n      <canvas ref={canvasRef} className=\"glow-cursor__canvas\" style={{ mixBlendMode: blendMode }} aria-hidden=\"true\" />\n      {children && <div className=\"glow-cursor__content\">{children}</div>}\n    </div>\n  );\n};\n\nexport default GlowCursor;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}