{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "HalftoneReveal-TS-CSS",
	"title": "HalftoneReveal",
	"description": "Print-style halftone dot matrix that resolves into sharp content around the cursor.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "HalftoneReveal.css",
			"target": "@components/HalftoneReveal.css",
			"content": ".halftone-reveal {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  touch-action: none;\n  cursor: crosshair;\n}\n\n.halftone-reveal canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "HalftoneReveal.tsx",
			"content": "import { useRef, useEffect, CSSProperties } from 'react';\nimport { Renderer, Program, Triangle, Mesh, Texture } from 'ogl';\n\nimport './HalftoneReveal.css';\n\nconst DEFAULT_SRC = 'https://picsum.photos/seed/halftone-reveal/1200/800';\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex || '');\n  return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [0, 0, 0];\n};\n\ntype Mode = 'mono' | 'duotone' | 'color';\ntype Shape = 'circle' | 'square' | 'diamond' | 'line';\ntype Trigger = 'off' | 'hover' | 'always';\n\nconst MODES: Record<Mode, number> = { mono: 0, duotone: 1, color: 2 };\nconst SHAPES: Record<Shape, number> = { circle: 0, square: 1, diamond: 2, line: 3 };\nconst TRIGGERS: Record<Trigger, number> = { off: 0, hover: 1, always: 2 };\n\nexport interface HalftoneRevealProps {\n  src?: string;\n  inkColor?: string;\n  paperColor?: string;\n  mode?: Mode;\n  dotSize?: number;\n  dotDensity?: number;\n  angle?: number;\n  shape?: Shape;\n  contrast?: number;\n  invert?: boolean;\n  revealRadius?: number;\n  edge?: number;\n  follow?: number;\n  idleReveal?: number;\n  trigger?: Trigger;\n  borderRadius?: string;\n  className?: string;\n  style?: CSSProperties;\n}\n\nconst vertex = `#version 300 es\nin vec2 position;\nout vec2 vUv;\nvoid main() {\n  vUv = position * 0.5 + 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tMap;\nuniform vec2 iResolution;\nuniform vec2 uImageSize;\nuniform vec2 uMouse;\nuniform float uActivity;\n\nuniform float uDotSize;\nuniform float uDensity;\nuniform float uAngle;\nuniform int uShape;\nuniform vec3 uInk;\nuniform vec3 uPaper;\nuniform int uMode;\nuniform float uContrast;\nuniform float uInvert;\n\nuniform float uRevealRadius;\nuniform float uEdge;\nuniform float uIdleReveal;\nuniform int uTrigger;\n\nin vec2 vUv;\nout vec4 fragColor;\n\nvec2 uAspect() {\n  return vec2(iResolution.x / max(iResolution.y, 1.0), 1.0);\n}\n\nvec2 coverUv(vec2 uv) {\n  float ia = uImageSize.x / max(uImageSize.y, 1.0);\n  float pa = iResolution.x / max(iResolution.y, 1.0);\n  vec2 s = pa > ia ? vec2(1.0, ia / pa) : vec2(pa / ia, 1.0);\n  return (uv - 0.5) * s + 0.5;\n}\n\nvec3 gradeRGB(vec3 c) {\n  c = clamp((c - 0.5) * uContrast + 0.5, 0.0, 1.0);\n  return mix(c, 1.0 - c, uInvert);\n}\n\nfloat shapeDist(vec2 f) {\n  if (uShape == 1) return max(abs(f.x), abs(f.y));\n  if (uShape == 2) return abs(f.x) + abs(f.y);\n  if (uShape == 3) return abs(f.y);\n  return length(f);\n}\n\nmat2 rot(float a) {\n  float c = cos(a);\n  float s = sin(a);\n  return mat2(c, -s, s, c);\n}\n\nvec4 sampleCell(vec2 st, float dens, float ang) {\n  vec2 rp = rot(ang) * st * dens;\n  vec2 center = floor(rp) + 0.5;\n  vec2 stC = rot(-ang) * (center / dens);\n  vec2 uvC = stC / uAspect();\n  return texture(tMap, clamp(coverUv(uvC), 0.0, 1.0));\n}\n\nfloat coverage(vec2 st, float dens, float ang, float ink, float rscale) {\n  vec2 rp = rot(ang) * st * dens;\n  vec2 f = fract(rp) - 0.5;\n  float d = shapeDist(f);\n  float r = sqrt(clamp(ink, 0.0, 1.0)) * 0.72 * rscale * uDotSize;\n  float w = length(fwidth(rp)) * 0.6 + 1e-4;\n  return smoothstep(r + w, r - w, d);\n}\n\nvoid main() {\n  vec2 aspect = uAspect();\n  vec2 st = vUv * aspect;\n  float ang = radians(uAngle);\n\n  vec2 duv = (vUv - uMouse) * aspect;\n  float dist = length(duv);\n\n  float act = uTrigger == 2 ? 1.0 : (uTrigger == 0 ? 0.0 : uActivity);\n  float radius = max(uRevealRadius, 1e-4) * mix(0.4, 1.0, act);\n\n  float px = 1.4 / max(iResolution.y, 1.0);\n  float band = max(px, radius * (1.0 - clamp(uEdge, 0.0, 1.0)) * 0.45);\n  float loupe = 1.0 - smoothstep(radius - band, radius + band, dist);\n  float focus = clamp(max(loupe * act, uIdleReveal), 0.0, 1.0);\n\n  float dens = uDensity;\n\n  vec3 print;\n  if (uMode == 2) {\n    vec3 gc = gradeRGB(sampleCell(st, dens, ang + radians(15.0)).rgb);\n    vec3 gm = gradeRGB(sampleCell(st, dens, ang + radians(75.0)).rgb);\n    vec3 gy = gradeRGB(sampleCell(st, dens, ang).rgb);\n    vec3 gk = gradeRGB(sampleCell(st, dens, ang + radians(45.0)).rgb);\n    float c = 1.0 - gc.r;\n    float m = 1.0 - gm.g;\n    float y = 1.0 - gy.b;\n    float k = 1.0 - dot(gk, vec3(0.299, 0.587, 0.114));\n    float gcr = min(min(c, m), y) * 0.5;\n    c = clamp(c - gcr, 0.0, 1.0);\n    m = clamp(m - gcr, 0.0, 1.0);\n    y = clamp(y - gcr, 0.0, 1.0);\n    k = clamp(max(gcr, k * k * 0.9), 0.0, 1.0);\n    float covC = coverage(st, dens, ang + radians(15.0), c, 0.82);\n    float covM = coverage(st, dens, ang + radians(75.0), m, 0.82);\n    float covY = coverage(st, dens, ang, y, 0.82);\n    float covK = coverage(st, dens, ang + radians(45.0), k, 0.78);\n    print = uPaper;\n    print = mix(print, print * vec3(0.10, 0.72, 0.90), covC);\n    print = mix(print, print * vec3(0.92, 0.10, 0.52), covM);\n    print = mix(print, print * vec3(0.98, 0.86, 0.10), covY);\n    print = mix(print, print * vec3(0.08), covK);\n  } else if (uMode == 1) {\n    vec3 ink2 = mix(uInk.gbr, vec3(0.90, 0.24, 0.30), 0.7);\n    float lumA = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n    float lumB = dot(gradeRGB(sampleCell(st, dens, ang + radians(38.0)).rgb), vec3(0.299, 0.587, 0.114));\n    float covA = coverage(st, dens, ang, 1.0 - lumA, 1.0);\n    float covB = coverage(st, dens, ang + radians(38.0), pow(1.0 - lumB, 1.4), 0.92);\n    print = uPaper;\n    print = mix(print, ink2, covB * 0.85);\n    print = mix(print, uInk, covA);\n  } else {\n    float lum = dot(gradeRGB(sampleCell(st, dens, ang).rgb), vec3(0.299, 0.587, 0.114));\n    float cov = coverage(st, dens, ang, 1.0 - lum, 1.0);\n    print = mix(uPaper, uInk, cov);\n  }\n\n  float t = clamp(dist / radius, 0.0, 1.0);\n  float bend = t * t * t * t;\n  vec2 dir = dist > 1e-5 ? duv / dist : vec2(0.0);\n  vec2 off = dir * bend * radius * 0.22 / aspect;\n  vec2 ca = dir * bend * 0.0045 / aspect;\n  vec3 sharp = gradeRGB(vec3(\n    texture(tMap, clamp(coverUv(vUv - off - ca), 0.0, 1.0)).r,\n    texture(tMap, clamp(coverUv(vUv - off), 0.0, 1.0)).g,\n    texture(tMap, clamp(coverUv(vUv - off + ca), 0.0, 1.0)).b\n  ));\n\n  vec3 col = mix(print, sharp, focus);\n  fragColor = vec4(col, 1.0);\n}\n`;\n\nconst HalftoneReveal = ({\n  src = DEFAULT_SRC,\n  inkColor = '#141414',\n  paperColor = '#fff7e6',\n  mode = 'mono',\n  dotSize = 1,\n  dotDensity = 71,\n  angle = 45,\n  shape = 'circle',\n  contrast = 1.15,\n  invert = false,\n  revealRadius = 0.4,\n  edge = 0.8,\n  follow = 0.37,\n  idleReveal = 0,\n  trigger = 'hover',\n  borderRadius = '16px',\n  className = '',\n  style\n}: HalftoneRevealProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const rendererRef = useRef<Renderer | null>(null);\n  const uniformsRef = useRef<Record<string, { value: unknown }> | null>(null);\n  const rafRef = useRef<number | null>(null);\n  const followRef = useRef<number>(follow);\n  const mouseRef = useRef({ x: 0.5, y: 0.5, sx: 0.5, sy: 0.5, active: 0, target: 0 });\n\n  useEffect(() => {\n    followRef.current = follow;\n  }, [follow]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const reduced =\n      typeof window !== 'undefined' &&\n      window.matchMedia &&\n      window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    const renderer = new Renderer({\n      dpr: Math.min(window.devicePixelRatio || 1, 2),\n      alpha: false,\n      antialias: true\n    });\n    rendererRef.current = renderer;\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 1);\n    gl.canvas.style.width = '100%';\n    gl.canvas.style.height = '100%';\n    gl.canvas.style.display = 'block';\n    container.appendChild(gl.canvas);\n\n    const texture = new Texture(gl, { generateMipmaps: false });\n\n    const uniforms = {\n      tMap: { value: texture },\n      iResolution: { value: [1, 1] },\n      uImageSize: { value: [1, 1] },\n      uMouse: { value: [0.5, 0.5] },\n      uActivity: { value: 0 },\n      uDotSize: { value: dotSize },\n      uDensity: { value: dotDensity },\n      uAngle: { value: angle },\n      uShape: { value: SHAPES[shape] ?? 0 },\n      uInk: { value: hexToRgb(inkColor) },\n      uPaper: { value: hexToRgb(paperColor) },\n      uMode: { value: MODES[mode] ?? 0 },\n      uContrast: { value: contrast },\n      uInvert: { value: invert ? 1 : 0 },\n      uRevealRadius: { value: revealRadius },\n      uEdge: { value: edge },\n      uIdleReveal: { value: idleReveal },\n      uTrigger: { value: TRIGGERS[trigger] ?? 1 }\n    };\n    uniformsRef.current = uniforms;\n\n    const program = new Program(gl, { vertex, fragment, uniforms });\n    const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });\n\n    const img = new Image();\n    img.crossOrigin = 'anonymous';\n    img.src = src;\n    img.onload = () => {\n      texture.image = img;\n      uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n    };\n\n    const resize = () => {\n      const w = container.clientWidth || 1;\n      const h = container.clientHeight || 1;\n      renderer.setSize(w, h);\n      uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height];\n    };\n    resize();\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n\n    const onMove = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      mouseRef.current.x = (e.clientX - rect.left) / rect.width;\n      mouseRef.current.y = 1 - (e.clientY - rect.top) / rect.height;\n      mouseRef.current.target = reduced ? 0 : 1;\n    };\n    const onLeave = () => {\n      mouseRef.current.target = 0;\n    };\n    container.addEventListener('pointermove', onMove, { passive: true });\n    container.addEventListener('pointerenter', onMove, { passive: true });\n    container.addEventListener('pointerleave', onLeave, { passive: true });\n\n    let prev = performance.now();\n    const loop = (now: number) => {\n      rafRef.current = requestAnimationFrame(loop);\n      const dt = Math.min(0.05, Math.max(0.001, (now - prev) / 1000));\n      prev = now;\n\n      const m = mouseRef.current;\n      const a = 1 - Math.exp(-dt / Math.max(0.001, followRef.current));\n      m.sx += (m.x - m.sx) * a;\n      m.sy += (m.y - m.sy) * a;\n      const ba = 1 - Math.exp(-dt / 0.18);\n      m.active += (m.target - m.active) * ba;\n\n      uniforms.uMouse.value[0] = m.sx;\n      uniforms.uMouse.value[1] = m.sy;\n      uniforms.uActivity.value = m.active;\n\n      renderer.render({ scene: mesh });\n    };\n    rafRef.current = requestAnimationFrame(loop);\n\n    return () => {\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      ro.disconnect();\n      container.removeEventListener('pointermove', onMove);\n      container.removeEventListener('pointerenter', onMove);\n      container.removeEventListener('pointerleave', onLeave);\n      const ext = gl.getExtension('WEBGL_lose_context');\n      if (ext) ext.loseContext();\n      if (gl.canvas.parentNode) gl.canvas.parentNode.removeChild(gl.canvas);\n      rendererRef.current = null;\n      uniformsRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [src]);\n\n  useEffect(() => {\n    const u = uniformsRef.current;\n    if (!u) return;\n    u.uDotSize.value = dotSize;\n    u.uDensity.value = dotDensity;\n    u.uAngle.value = angle;\n    u.uShape.value = SHAPES[shape] ?? 0;\n    u.uInk.value = hexToRgb(inkColor);\n    u.uPaper.value = hexToRgb(paperColor);\n    u.uMode.value = MODES[mode] ?? 0;\n    u.uContrast.value = contrast;\n    u.uInvert.value = invert ? 1 : 0;\n    u.uRevealRadius.value = revealRadius;\n    u.uEdge.value = edge;\n    u.uIdleReveal.value = idleReveal;\n    u.uTrigger.value = TRIGGERS[trigger] ?? 1;\n  }, [\n    dotSize,\n    dotDensity,\n    angle,\n    shape,\n    inkColor,\n    paperColor,\n    mode,\n    contrast,\n    invert,\n    revealRadius,\n    edge,\n    idleReveal,\n    trigger\n  ]);\n\n  return (\n    <div ref={containerRef} className={`halftone-reveal ${className}`.trim()} style={{ borderRadius, ...style }} />\n  );\n};\n\nexport default HalftoneReveal;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}