{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SpecularButton-TS-CSS",
	"title": "SpecularButton",
	"description": "Glass button with a shader-driven specular rim light that sweeps around the edge and follows the cursor.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "SpecularButton.css",
			"target": "@components/SpecularButton.css",
			"content": ".specular-button {\n  --sb-radius: 18px;\n  --sb-tint: #ffffff;\n  --sb-tint-opacity: 0;\n  --sb-blur: 0px;\n  --sb-text-color: #f5f5f5;\n\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border: none;\n  margin: 0;\n  font-family: inherit;\n  font-weight: 500;\n  letter-spacing: 0.01em;\n  line-height: 1;\n  color: var(--sb-text-color);\n  background: color-mix(in srgb, var(--sb-tint) calc(var(--sb-tint-opacity) * 100%), transparent);\n  border-radius: var(--sb-radius);\n  backdrop-filter: blur(var(--sb-blur));\n  -webkit-backdrop-filter: blur(var(--sb-blur));\n  box-shadow:\n    inset 0 1px 0 rgba(255, 255, 255, 0.04),\n    0 8px 24px rgba(0, 0, 0, 0.25);\n  cursor: pointer;\n  outline: none;\n  transition: transform 0.15s ease;\n}\n\n.specular-button:active {\n  transform: scale(0.97);\n}\n\n.specular-button:focus-visible {\n  outline: 2px solid color-mix(in srgb, var(--sb-text-color) 60%, transparent);\n  outline-offset: 3px;\n}\n\n.specular-button:disabled {\n  opacity: 0.55;\n  cursor: default;\n}\n\n.specular-button:disabled:active {\n  transform: none;\n}\n\n.specular-button--sm {\n  font-size: 0.85rem;\n  padding: 10px 22px;\n}\n\n.specular-button--md {\n  font-size: 1rem;\n  padding: 14px 30px;\n}\n\n.specular-button--lg {\n  font-size: 1.15rem;\n  padding: 18px 40px;\n}\n\n/* Canvas extends past the button so the rim glow can bleed outside the edge */\n.specular-button__fx {\n  position: absolute;\n  inset: -20px;\n  pointer-events: none;\n  z-index: 1;\n}\n\n.specular-button__fx canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.specular-button__label {\n  position: relative;\n  z-index: 2;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "SpecularButton.tsx",
			"content": "import { useRef, useEffect, type CSSProperties, type ReactNode, type MouseEventHandler } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Color } from 'ogl';\nimport './SpecularButton.css';\n\ntype ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface SpecularButtonProps {\n  children?: ReactNode;\n  size?: ButtonSize;\n  radius?: number;\n  tint?: string;\n  tintOpacity?: number;\n  blur?: number;\n  textColor?: string;\n  lineColor?: string;\n  baseColor?: string;\n  intensity?: number;\n  shineSize?: number;\n  shineFade?: number;\n  thickness?: number;\n  speed?: number;\n  followMouse?: boolean;\n  proximity?: number;\n  autoAnimate?: boolean;\n  disabled?: boolean;\n  onClick?: MouseEventHandler<HTMLButtonElement>;\n  className?: string;\n  type?: 'button' | 'submit' | 'reset';\n}\n\ninterface ShaderProps {\n  radius: number;\n  lineColor: string;\n  baseColor: string;\n  intensity: number;\n  shineSize: number;\n  shineFade: number;\n  thickness: number;\n  speed: number;\n  followMouse: boolean;\n  proximity: number;\n  autoAnimate: boolean;\n}\n\nconst PAD = 20;\n\nconst VERT = `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\n\nuniform vec2 uCenter;\nuniform vec2 uHalfSize;\nuniform float uRadius;\nuniform float uAngle;\nuniform float uPx;\nuniform vec3 uLineColor;\nuniform vec3 uBaseColor;\nuniform float uIntensity;\nuniform float uShineSize;\nuniform float uShineFade;\nuniform float uThickness;\nuniform float uBaseWidth;\n\nout vec4 fragColor;\n\nfloat sdRoundedRect(vec2 p, vec2 b, float r) {\n  vec2 q = abs(p) - b + r;\n  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n}\n\nfloat shapeSDF(vec2 p) { return sdRoundedRect(p, uHalfSize, uRadius); }\n\nfloat gaussianLine(float d, float sigma) {\n  float x = d / (sigma + 1e-6);\n  float k = mix(1.0, 1.6, smoothstep(0.0, 1.5, x));\n  return exp(-k * x * x);\n}\n\nvoid main() {\n  vec2 p = gl_FragCoord.xy - uCenter;\n  float d = shapeSDF(p);\n  vec2 L = vec2(cos(uAngle), sin(uAngle));\n\n  // Dark base stroke hugging the edge for a sense of thickness\n  float base = (1.0 - smoothstep(0.0, uBaseWidth, abs(d))) * 0.45;\n\n  // Symmetric specular: the edges facing toward/away from the light both\n  // catch a streak. The angular window (size + fade) is measured with an\n  // elliptical normal so it varies continuously along straight edges.\n  vec2 nEll = normalize(p / (uHalfSize * uHalfSize) + 1e-6);\n  float phi = acos(clamp(abs(dot(nEll, L)), 0.0, 1.0));\n  float rim = 1.0 - smoothstep(uShineSize - uShineFade, uShineSize + uShineFade + 1e-4, phi);\n  float line = gaussianLine(d, uThickness);\n  float edgeClamp = 1.0 - smoothstep(0.5 * uPx, 3.0 * uPx, abs(d));\n  float hi = line * rim * edgeClamp * uIntensity;\n\n  vec3 col = uBaseColor * base + uLineColor * hi;\n  float a = clamp(base + hi, 0.0, 1.0);\n  fragColor = vec4(col, a);\n}\n`;\n\nconst SpecularButton = ({\n  children = 'Get Started',\n  size = 'lg',\n  radius = 18,\n  tint = '#ffffff',\n  tintOpacity = 0,\n  blur = 0,\n  textColor = '#f5f5f5',\n  lineColor = '#ffffff',\n  baseColor = '#525252',\n  intensity = 1,\n  shineSize = 10,\n  shineFade = 40,\n  thickness = 1,\n  speed = 0.35,\n  followMouse = true,\n  proximity = 250,\n  autoAnimate = false,\n  disabled = false,\n  onClick,\n  className = '',\n  type = 'button'\n}: SpecularButtonProps) => {\n  const btnRef = useRef<HTMLButtonElement>(null);\n  const fxRef = useRef<HTMLSpanElement>(null);\n  const propsRef = useRef<ShaderProps>({} as ShaderProps);\n\n  propsRef.current = { radius, lineColor, baseColor, intensity, shineSize, shineFade, thickness, speed, followMouse, proximity, autoAnimate };\n\n  useEffect(() => {\n    const btn = btnRef.current;\n    const fx = fxRef.current;\n    if (!btn || !fx) return;\n\n    const dpr = window.devicePixelRatio || 1;\n    const renderer = new Renderer({ alpha: true, premultipliedAlpha: true, antialias: true, dpr });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 0);\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n    const geometry = new Triangle(gl);\n    if (geometry.attributes.uv) delete geometry.attributes.uv;\n\n    const program = new Program(gl, {\n      vertex: VERT,\n      fragment: FRAG,\n      uniforms: {\n        uCenter: { value: [0, 0] },\n        uHalfSize: { value: [1, 1] },\n        uRadius: { value: 0 },\n        uAngle: { value: 2.4 },\n        uPx: { value: dpr },\n        uLineColor: { value: [1, 1, 1] },\n        uBaseColor: { value: [0.32, 0.32, 0.32] },\n        uIntensity: { value: 1 },\n        uShineSize: { value: 0.17 },\n        uShineFade: { value: 0.7 },\n        uThickness: { value: 1 },\n\n        uBaseWidth: { value: dpr }\n      }\n    });\n\n    const mesh = new Mesh(gl, { geometry, program });\n    fx.appendChild(gl.canvas);\n\n    const sizeRef = { w: 1, h: 1 };\n    const resize = () => {\n      // Fractional size + explicit center keep the SDF pinned to the exact\n      // CSS border, instead of drifting up to a pixel from offsetWidth rounding.\n      const rect = btn.getBoundingClientRect();\n      const w = rect.width;\n      const h = rect.height;\n      sizeRef.w = w;\n      sizeRef.h = h;\n      renderer.setSize(w + PAD * 2, h + PAD * 2);\n      program.uniforms.uCenter.value = [(PAD + w / 2) * dpr, (PAD + h / 2) * dpr];\n      program.uniforms.uHalfSize.value = [(w / 2) * dpr, (h / 2) * dpr];\n    };\n    const ro = new ResizeObserver(resize);\n    ro.observe(btn);\n    resize();\n\n    // Light angle steers toward the pointer (anywhere on the page) and falls\n    // back to a slow sweep when the pointer hasn't moved yet.\n    let pointerAngle: number | null = null;\n    let proximityT = 0;\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = btn.getBoundingClientRect();\n      const cx = rect.left + rect.width / 2;\n      const cy = rect.top + rect.height / 2;\n      const dx = Math.max(rect.left - e.clientX, 0, e.clientX - rect.right);\n      const dy = Math.max(rect.top - e.clientY, 0, e.clientY - rect.bottom);\n      const dist = Math.hypot(dx, dy);\n      // Over the button itself the light settles on the diagonal (framing the\n      // corners) and gently sways with the cursor position within the button.\n      if (dist === 0) {\n        const nx = (e.clientX - cx) / (rect.width / 2);\n        const ny = (cy - e.clientY) / (rect.height / 2);\n        pointerAngle = Math.atan2(2 / rect.height, -2 / rect.width) + nx * 0.3 + ny * 0.15;\n      } else {\n        pointerAngle = Math.atan2(cy - e.clientY, e.clientX - cx);\n      }\n      const t = Math.max(0, 1 - dist / Math.max(propsRef.current.proximity, 1));\n      proximityT = t * t * (3 - 2 * t);\n    };\n    window.addEventListener('pointermove', onPointerMove);\n\n    let angle = 2.4;\n    let idleAngle = 2.4;\n    let bright = 0;\n    let last = performance.now();\n    let raf = 0;\n\n    const lineC = new Color();\n    const baseC = new Color();\n\n    const update = (now: number) => {\n      raf = requestAnimationFrame(update);\n      const dt = Math.min((now - last) / 1000, 0.05);\n      last = now;\n      const p = propsRef.current;\n\n      idleAngle += p.speed * dt;\n      const target =\n        p.followMouse && pointerAngle != null && (!p.autoAnimate || proximityT > 0) ? pointerAngle : idleAngle;\n      const diff = ((target - angle + Math.PI * 3) % (Math.PI * 2)) - Math.PI;\n      angle += diff * (1 - Math.exp(-dt * 7));\n\n      // Shine fades in with pointer proximity unless autoAnimate keeps it on\n      const brightTarget = p.autoAnimate ? 1 : proximityT;\n      bright += (brightTarget - bright) * (1 - Math.exp(-dt * 8));\n\n      lineC.set(p.lineColor);\n      baseC.set(p.baseColor);\n      program.uniforms.uAngle.value = angle;\n      program.uniforms.uRadius.value = Math.min(p.radius, Math.min(sizeRef.w, sizeRef.h) / 2) * dpr;\n      program.uniforms.uLineColor.value = [lineC.r, lineC.g, lineC.b];\n      program.uniforms.uBaseColor.value = [baseC.r, baseC.g, baseC.b];\n      program.uniforms.uIntensity.value = p.intensity * bright;\n      program.uniforms.uShineSize.value = (p.shineSize * Math.PI) / 180;\n      program.uniforms.uShineFade.value = (p.shineFade * Math.PI) / 180;\n      program.uniforms.uThickness.value = p.thickness * dpr;\n      renderer.render({ scene: mesh });\n    };\n    raf = requestAnimationFrame(update);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      window.removeEventListener('pointermove', onPointerMove);\n      if (gl.canvas.parentNode === fx) fx.removeChild(gl.canvas);\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n    };\n  }, []);\n\n  return (\n    <button\n      ref={btnRef}\n      type={type}\n      disabled={disabled}\n      onClick={onClick}\n      className={`specular-button specular-button--${size}${className ? ` ${className}` : ''}`}\n      style={\n        {\n          '--sb-radius': `${radius}px`,\n          '--sb-tint': tint,\n          '--sb-tint-opacity': tintOpacity,\n          '--sb-blur': `${blur}px`,\n          '--sb-text-color': textColor\n        } as CSSProperties\n      }\n    >\n      <span ref={fxRef} className=\"specular-button__fx\" aria-hidden=\"true\" />\n      <span className=\"specular-button__label\">{children}</span>\n    </button>\n  );\n};\n\nexport default SpecularButton;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}