{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GridScan-TS-CSS",
	"title": "GridScan",
	"description": "Animated grid room 3D scan effect and cool interactions.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "GridScan.css",
			"target": "@components/GridScan.css",
			"content": ".gridscan {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.gridscan__preview {\n  position: absolute;\n  right: 12px;\n  bottom: 12px;\n  width: 220px;\n  height: 132px;\n  border-radius: 8px;\n  overflow: hidden;\n  border: 1px solid rgba(255, 255, 255, 0.25);\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n  background: #000;\n  color: #fff;\n  font:\n    12px/1.2 system-ui,\n    -apple-system,\n    Segoe UI,\n    Roboto,\n    sans-serif;\n  pointer-events: none;\n}\n\n.gridscan__video {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  transform: scaleX(-1);\n}\n\n.gridscan__badge {\n  position: absolute;\n  left: 8px;\n  top: 8px;\n  padding: 2px 6px;\n  background: rgba(0, 0, 0, 0.5);\n  border-radius: 6px;\n  backdrop-filter: blur(4px);\n}\n"
		},
		{
			"type": "registry:component",
			"path": "GridScan.tsx",
			"content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport React, { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport './GridScan.css';\n\ntype GridScanProps = {\n  enableWebcam?: boolean;\n  showPreview?: boolean;\n  modelsPath?: string;\n  sensitivity?: number;\n\n  lineThickness?: number;\n  linesColor?: string;\n\n  gridScale?: number;\n  lineStyle?: 'solid' | 'dashed' | 'dotted';\n  lineJitter?: number;\n\n  enablePost?: boolean;\n  bloomIntensity?: number;\n  bloomThreshold?: number;\n  bloomSmoothing?: number;\n  chromaticAberration?: number;\n  noiseIntensity?: number;\n\n  scanColor?: string;\n  scanOpacity?: number;\n  scanDirection?: 'forward' | 'backward' | 'pingpong';\n  scanSoftness?: number;\n  scanGlow?: number;\n  scanPhaseTaper?: number;\n  scanDuration?: number;\n  scanDelay?: number;\n  enableGyro?: boolean;\n  scanOnClick?: boolean;\n  snapBackDelay?: number;\n  lightMode?: boolean;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n  vUv = uv;\n  gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nuniform float uLightMode;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n  float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n    vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n    vec3 ro = vec3(0.0);\n    vec3 rd = normalize(vec3(p, 2.0));\n\n    float cR = cos(uTilt), sR = sin(uTilt);\n    rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n    float cY = cos(uYaw), sY = sin(uYaw);\n    rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n    vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n    rd.xy += skew * rd.z;\n\n    vec3 color = vec3(0.0);\n  float minT = 1e20;\n  float gridScale = max(1e-5, uGridScale);\n    float fadeStrength = 2.0;\n    vec2 gridUV = vec2(0.0);\n\n  float hitIsY = 1.0;\n    for (int i = 0; i < 4; i++)\n    {\n        float isY = float(i < 2);\n        float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n        float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n        float den = isY * rd.y + (1.0 - isY) * rd.x;\n        float t = num / den;\n        vec3 h = ro + rd * t;\n\n        float depthBoost = smoothstep(0.0, 3.0, h.z);\n        h.xy += skew * 0.15 * depthBoost;\n\n    bool use = t > 0.0 && t < minT;\n    gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n    minT = use ? t : minT;\n    hitIsY = use ? isY : hitIsY;\n    }\n\n    vec3 hit = ro + rd * minT;\n    float dist = length(hit - ro);\n\n  float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n  if (jitterAmt > 0.0) {\n    vec2 j = vec2(\n      sin(gridUV.y * 2.7 + iTime * 1.8),\n      cos(gridUV.x * 2.3 - iTime * 1.6)\n    ) * (0.15 * jitterAmt);\n    gridUV += j;\n  }\n  float fx = fract(gridUV.x);\n  float fy = fract(gridUV.y);\n  float ax = min(fx, 1.0 - fx);\n  float ay = min(fy, 1.0 - fy);\n  float wx = fwidth(gridUV.x);\n  float wy = fwidth(gridUV.y);\n  float halfPx = max(0.0, uLineThickness) * 0.5;\n\n  float tx = halfPx * wx;\n  float ty = halfPx * wy;\n\n  float aax = wx;\n  float aay = wy;\n\n  float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n  float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n  if (uLineStyle > 0.5) {\n    float dashRepeat = 4.0;\n    float dashDuty = 0.5;\n    float vy = fract(gridUV.y * dashRepeat);\n    float vx = fract(gridUV.x * dashRepeat);\n    float dashMaskY = step(vy, dashDuty);\n    float dashMaskX = step(vx, dashDuty);\n    if (uLineStyle < 1.5) {\n      lineX *= dashMaskY;\n      lineY *= dashMaskX;\n    } else {\n      float dotRepeat = 6.0;\n      float dotWidth = 0.18;\n      float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n      float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n      float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n      float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n      lineX *= dotMaskY;\n      lineY *= dotMaskX;\n    }\n  }\n  float primaryMask = max(lineX, lineY);\n\n  vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n  if (jitterAmt > 0.0) {\n    vec2 j2 = vec2(\n      cos(gridUV2.y * 2.1 - iTime * 1.4),\n      sin(gridUV2.x * 2.5 + iTime * 1.7)\n    ) * (0.15 * jitterAmt);\n    gridUV2 += j2;\n  }\n  float fx2 = fract(gridUV2.x);\n  float fy2 = fract(gridUV2.y);\n  float ax2 = min(fx2, 1.0 - fx2);\n  float ay2 = min(fy2, 1.0 - fy2);\n  float wx2 = fwidth(gridUV2.x);\n  float wy2 = fwidth(gridUV2.y);\n  float tx2 = halfPx * wx2;\n  float ty2 = halfPx * wy2;\n  float aax2 = wx2;\n  float aay2 = wy2;\n  float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n  float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n  if (uLineStyle > 0.5) {\n    float dashRepeat2 = 4.0;\n    float dashDuty2 = 0.5;\n    float vy2m = fract(gridUV2.y * dashRepeat2);\n    float vx2m = fract(gridUV2.x * dashRepeat2);\n    float dashMaskY2 = step(vy2m, dashDuty2);\n    float dashMaskX2 = step(vx2m, dashDuty2);\n    if (uLineStyle < 1.5) {\n      lineX2 *= dashMaskY2;\n      lineY2 *= dashMaskX2;\n    } else {\n      float dotRepeat2 = 6.0;\n      float dotWidth2 = 0.18;\n      float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n      float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n      float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n      float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n      lineX2 *= dotMaskY2;\n      lineY2 *= dotMaskX2;\n    }\n  }\n    float altMask = max(lineX2, lineY2);\n\n    float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n    float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n    float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n    float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n    altMask *= edgeGate;\n\n  float lineMask = max(primaryMask, altMask);\n\n    float fade = exp(-dist * fadeStrength);\n\n    float dur = max(0.05, uScanDuration);\n    float del = max(0.0, uScanDelay);\n    float scanZMax = 2.0;\n    float widthScale = max(0.1, uScanGlow);\n    float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n    float sigmaA = sigma * 2.0;\n\n    float combinedPulse = 0.0;\n    float combinedAura = 0.0;\n\n    float cycle = dur + del;\n    float tCycle = mod(iTime, cycle);\n    float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n    float phase = scanPhase;\n    if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n      phase = 1.0 - phase;\n    } else if (uScanDirection > 1.5) {\n      float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n      phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n    }\n    float scanZ = phase * scanZMax;\n    float dz = abs(hit.z - scanZ);\n    float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n    float taper = clamp(uPhaseTaper, 0.0, 0.49);\n    float headW = taper;\n    float tailW = taper;\n    float headFade = smoother01(0.0, headW, phase);\n    float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n    float phaseWindow = headFade * tailFade;\n    float pulseBase = lineBand * phaseWindow;\n    combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n    float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n    combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n    for (int i = 0; i < MAX_SCANS; i++) {\n      if (float(i) >= uScanCount) break;\n      float tActiveI = iTime - uScanStarts[i];\n      float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n      if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n        phaseI = 1.0 - phaseI;\n      } else if (uScanDirection > 1.5) {\n        phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n      }\n      float scanZI = phaseI * scanZMax;\n      float dzI = abs(hit.z - scanZI);\n      float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n      float headFadeI = smoother01(0.0, headW, phaseI);\n      float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n      float phaseWindowI = headFadeI * tailFadeI;\n      combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n      float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n      combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n    }\n\n  float lineVis = lineMask;\n  vec3 gridCol = uLinesColor * lineVis * fade;\n  vec3 scanCol = uScanColor * combinedPulse;\n  vec3 scanAura = uScanColor * combinedAura;\n\n    color = gridCol + scanCol + scanAura;\n\n  float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n  color += (n - 0.5) * uNoise;\n  color = clamp(color, 0.0, 1.0);\n  float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n  float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n  float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n  float halo = max(gx, gy) * fade;\n  alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n  if (uLightMode > 0.5) {\n    float energy = max(max(color.r, color.g), color.b);\n    float coverage = clamp(max(alpha, smoothstep(0.0, 0.55, energy) * 0.82), 0.0, 0.9);\n    coverage *= smoothstep(0.015, 0.12, energy);\n    vec3 chroma = clamp(color / max(energy, 0.0001), 0.0, 1.0);\n    chroma = pow(chroma, vec3(1.2));\n    fragColor = vec4(mix(vec3(1.0), chroma, coverage * 0.94), 1.0);\n  } else {\n    fragColor = vec4(color, alpha);\n  }\n}\n\nvoid main(){\n  vec4 c;\n  mainImage(c, vUv * iResolution.xy);\n  gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC<GridScanProps> = ({\n  enableWebcam = false,\n  showPreview = false,\n  modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n  sensitivity = 0.55,\n  lineThickness = 1,\n  linesColor = '#2F293A',\n  scanColor = '#FF9FFC',\n  scanOpacity = 0.4,\n  gridScale = 0.1,\n  lineStyle = 'solid',\n  lineJitter = 0.1,\n  scanDirection = 'pingpong',\n  enablePost = true,\n  bloomIntensity = 0,\n  bloomThreshold = 0,\n  bloomSmoothing = 0,\n  chromaticAberration = 0.002,\n  noiseIntensity = 0.01,\n  scanGlow = 0.5,\n  scanSoftness = 2,\n  scanPhaseTaper = 0.9,\n  scanDuration = 2.0,\n  scanDelay = 2.0,\n  enableGyro = false,\n  scanOnClick = false,\n  snapBackDelay = 250,\n  lightMode = false,\n  className,\n  style\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const videoRef = useRef<HTMLVideoElement | null>(null);\n\n  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);\n  const materialRef = useRef<THREE.ShaderMaterial | null>(null);\n  const composerRef = useRef<EffectComposer | null>(null);\n  const bloomRef = useRef<BloomEffect | null>(null);\n  const chromaRef = useRef<ChromaticAberrationEffect | null>(null);\n  const rafRef = useRef<number | null>(null);\n\n  const [modelsReady, setModelsReady] = useState(false);\n  const [uiFaceActive, setUiFaceActive] = useState(false);\n\n  const lookTarget = useRef(new THREE.Vector2(0, 0));\n  const tiltTarget = useRef(0);\n  const yawTarget = useRef(0);\n\n  const lookCurrent = useRef(new THREE.Vector2(0, 0));\n  const lookVel = useRef(new THREE.Vector2(0, 0));\n  const tiltCurrent = useRef(0);\n  const tiltVel = useRef(0);\n  const yawCurrent = useRef(0);\n  const yawVel = useRef(0);\n\n  const MAX_SCANS = 8;\n  const scanStartsRef = useRef<number[]>([]);\n\n  const pushScan = (t: number) => {\n    const arr = scanStartsRef.current.slice();\n    if (arr.length >= MAX_SCANS) arr.shift();\n    arr.push(t);\n    scanStartsRef.current = arr;\n    if (materialRef.current) {\n      const u = materialRef.current.uniforms;\n      const buf = new Array(MAX_SCANS).fill(0);\n      for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n      u.uScanStarts.value = buf;\n      u.uScanCount.value = arr.length;\n    }\n  };\n\n  const bufX = useRef<number[]>([]);\n  const bufY = useRef<number[]>([]);\n  const bufT = useRef<number[]>([]);\n  const bufYaw = useRef<number[]>([]);\n\n  const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n  const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n  const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n  const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n  const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n  const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n  const maxSpeed = Infinity;\n\n  const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return;\n    let leaveTimer: number | null = null;\n    const onMove = (e: MouseEvent) => {\n      if (uiFaceActive) return;\n      if (leaveTimer) {\n        clearTimeout(leaveTimer);\n        leaveTimer = null;\n      }\n      const rect = el.getBoundingClientRect();\n      const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n      const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n      lookTarget.current.set(nx, ny);\n    };\n    const onClick = async () => {\n      const nowSec = performance.now() / 1000;\n      if (scanOnClick) pushScan(nowSec);\n      if (\n        enableGyro &&\n        typeof window !== 'undefined' &&\n        (window as any).DeviceOrientationEvent &&\n        (DeviceOrientationEvent as any).requestPermission\n      ) {\n        try {\n          await (DeviceOrientationEvent as any).requestPermission();\n        } catch {}\n      }\n    };\n    const onEnter = () => {\n      if (leaveTimer) {\n        clearTimeout(leaveTimer);\n        leaveTimer = null;\n      }\n    };\n    const onLeave = () => {\n      if (uiFaceActive) return;\n      if (leaveTimer) clearTimeout(leaveTimer);\n      leaveTimer = window.setTimeout(\n        () => {\n          lookTarget.current.set(0, 0);\n          tiltTarget.current = 0;\n          yawTarget.current = 0;\n        },\n        Math.max(0, snapBackDelay || 0)\n      );\n    };\n    el.addEventListener('mousemove', onMove);\n    el.addEventListener('mouseenter', onEnter);\n    if (scanOnClick) el.addEventListener('click', onClick);\n    el.addEventListener('mouseleave', onLeave);\n    return () => {\n      el.removeEventListener('mousemove', onMove);\n      el.removeEventListener('mouseenter', onEnter);\n      el.removeEventListener('mouseleave', onLeave);\n      if (scanOnClick) el.removeEventListener('click', onClick);\n      if (leaveTimer) clearTimeout(leaveTimer);\n    };\n  }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n    rendererRef.current = renderer;\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n    renderer.setSize(container.clientWidth, container.clientHeight);\n    renderer.outputColorSpace = THREE.SRGBColorSpace;\n    renderer.toneMapping = THREE.NoToneMapping;\n    renderer.autoClear = false;\n    renderer.setClearColor(0x000000, 0);\n    container.appendChild(renderer.domElement);\n\n    const uniforms = {\n      iResolution: {\n        value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n      },\n      iTime: { value: 0 },\n      uSkew: { value: new THREE.Vector2(0, 0) },\n      uTilt: { value: 0 },\n      uYaw: { value: 0 },\n      uLineThickness: { value: lineThickness },\n      uLinesColor: { value: srgbColor(linesColor) },\n      uScanColor: { value: srgbColor(scanColor) },\n      uGridScale: { value: gridScale },\n      uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n      uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n      uScanOpacity: { value: scanOpacity },\n      uNoise: { value: noiseIntensity },\n      uBloomOpacity: { value: bloomIntensity },\n      uScanGlow: { value: scanGlow },\n      uScanSoftness: { value: scanSoftness },\n      uPhaseTaper: { value: scanPhaseTaper },\n      uScanDuration: { value: scanDuration },\n      uScanDelay: { value: scanDelay },\n      uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n      uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n      uScanCount: { value: 0 },\n      uLightMode: { value: lightMode ? 1 : 0 }\n    };\n\n    const material = new THREE.ShaderMaterial({\n      uniforms,\n      vertexShader: vert,\n      fragmentShader: frag,\n      transparent: true,\n      depthWrite: false,\n      depthTest: false\n    });\n    materialRef.current = material;\n\n    const scene = new THREE.Scene();\n    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n    const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n    scene.add(quad);\n\n    let composer: EffectComposer | null = null;\n    if (enablePost) {\n      composer = new EffectComposer(renderer);\n      composerRef.current = composer;\n      const renderPass = new RenderPass(scene, camera);\n      composer.addPass(renderPass);\n\n      const bloom = new BloomEffect({\n        intensity: 1.0,\n        luminanceThreshold: bloomThreshold,\n        luminanceSmoothing: bloomSmoothing\n      });\n      bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n      bloomRef.current = bloom;\n\n      const chroma = new ChromaticAberrationEffect({\n        offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n        radialModulation: true,\n        modulationOffset: 0.0\n      });\n      chromaRef.current = chroma;\n\n      const effectPass = new EffectPass(camera, bloom, chroma);\n      effectPass.renderToScreen = true;\n      composer.addPass(effectPass);\n    }\n\n    const onResize = () => {\n      renderer.setSize(container.clientWidth, container.clientHeight);\n      material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n      if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n    };\n    window.addEventListener('resize', onResize);\n\n    let last = performance.now();\n    const tick = () => {\n      const now = performance.now();\n      const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n      last = now;\n\n      lookCurrent.current.copy(\n        smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n      );\n\n      const tiltSm = smoothDampFloat(\n        tiltCurrent.current,\n        tiltTarget.current,\n        { v: tiltVel.current },\n        smoothTime,\n        maxSpeed,\n        dt\n      );\n      tiltCurrent.current = tiltSm.value;\n      tiltVel.current = tiltSm.v;\n\n      const yawSm = smoothDampFloat(\n        yawCurrent.current,\n        yawTarget.current,\n        { v: yawVel.current },\n        smoothTime,\n        maxSpeed,\n        dt\n      );\n      yawCurrent.current = yawSm.value;\n      yawVel.current = yawSm.v;\n\n      const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n      material.uniforms.uSkew.value.set(skew.x, skew.y);\n      material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n      material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n      material.uniforms.iTime.value = now / 1000;\n      renderer.clear(true, true, true);\n      if (composerRef.current) {\n        composerRef.current.render(dt);\n      } else {\n        renderer.render(scene, camera);\n      }\n      rafRef.current = requestAnimationFrame(tick);\n    };\n    rafRef.current = requestAnimationFrame(tick);\n\n    return () => {\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      window.removeEventListener('resize', onResize);\n      material.dispose();\n      (quad.geometry as THREE.BufferGeometry).dispose();\n      if (composerRef.current) {\n        composerRef.current.dispose();\n        composerRef.current = null;\n      }\n      renderer.dispose();\n      renderer.forceContextLoss();\n      container.removeChild(renderer.domElement);\n    };\n  }, [\n    sensitivity,\n    lineThickness,\n    linesColor,\n    scanColor,\n    scanOpacity,\n    gridScale,\n    lineStyle,\n    lineJitter,\n    scanDirection,\n    enablePost\n  ]);\n\n  useEffect(() => {\n    const m = materialRef.current;\n    if (m) {\n      const u = m.uniforms;\n      u.uLineThickness.value = lineThickness;\n      (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n      (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n      u.uGridScale.value = gridScale;\n      u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n      u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n      u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n      u.uNoise.value = Math.max(0, noiseIntensity);\n      u.uScanGlow.value = scanGlow;\n      u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n      u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n      u.uScanSoftness.value = scanSoftness;\n      u.uPhaseTaper.value = scanPhaseTaper;\n      u.uScanDuration.value = Math.max(0.05, scanDuration);\n      u.uScanDelay.value = Math.max(0.0, scanDelay);\n      u.uLightMode.value = lightMode ? 1 : 0;\n    }\n    if (bloomRef.current) {\n      bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n      (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n      (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n    }\n    if (chromaRef.current) {\n      chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n    }\n  }, [\n    lineThickness,\n    linesColor,\n    scanColor,\n    gridScale,\n    lineStyle,\n    lineJitter,\n    bloomIntensity,\n    bloomThreshold,\n    bloomSmoothing,\n    chromaticAberration,\n    noiseIntensity,\n    scanGlow,\n    scanOpacity,\n    scanDirection,\n    scanSoftness,\n    scanPhaseTaper,\n    scanDuration,\n    scanDelay,\n    lightMode\n  ]);\n\n  useEffect(() => {\n    if (!enableGyro) return;\n    const handler = (e: DeviceOrientationEvent) => {\n      if (uiFaceActive) return;\n      const gamma = e.gamma ?? 0;\n      const beta = e.beta ?? 0;\n      const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n      const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n      lookTarget.current.set(nx, ny);\n      tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n    };\n    window.addEventListener('deviceorientation', handler);\n    return () => {\n      window.removeEventListener('deviceorientation', handler);\n    };\n  }, [enableGyro, uiFaceActive]);\n\n  useEffect(() => {\n    let canceled = false;\n    const load = async () => {\n      try {\n        await Promise.all([\n          faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n          faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n        ]);\n        if (!canceled) setModelsReady(true);\n      } catch {\n        if (!canceled) setModelsReady(false);\n      }\n    };\n    load();\n    return () => {\n      canceled = true;\n    };\n  }, [modelsPath]);\n\n  useEffect(() => {\n    let stop = false;\n    let lastDetect = 0;\n\n    const start = async () => {\n      if (!enableWebcam || !modelsReady) return;\n      const video = videoRef.current;\n      if (!video) return;\n\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n          audio: false\n        });\n        video.srcObject = stream;\n        await video.play();\n      } catch {\n        return;\n      }\n\n      const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n      const detect = async (ts: number) => {\n        if (stop) return;\n\n        if (ts - lastDetect >= 33) {\n          lastDetect = ts;\n          try {\n            const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n            if (res && res.detection) {\n              const det = res.detection;\n              const box = det.box;\n              const vw = video.videoWidth || 1;\n              const vh = video.videoHeight || 1;\n\n              const cx = box.x + box.width * 0.5;\n              const cy = box.y + box.height * 0.5;\n              const nx = (cx / vw) * 2 - 1;\n              const ny = (cy / vh) * 2 - 1;\n              medianPush(bufX.current, nx, 5);\n              medianPush(bufY.current, ny, 5);\n              const nxm = median(bufX.current);\n              const nym = median(bufY.current);\n\n              const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n              const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n              const depthScale = 1 + depthResponse * (faceSize - 0.25);\n              lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n              const leftEye = res.landmarks.getLeftEye();\n              const rightEye = res.landmarks.getRightEye();\n              const lc = centroid(leftEye);\n              const rc = centroid(rightEye);\n              const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n              medianPush(bufT.current, tilt, 5);\n              tiltTarget.current = median(bufT.current);\n\n              const nose = res.landmarks.getNose();\n              const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n              const jaw = res.landmarks.getJawOutline();\n              const leftCheek = jaw[3] || jaw[2];\n              const rightCheek = jaw[13] || jaw[14];\n              const dL = dist2(tip, leftCheek);\n              const dR = dist2(tip, rightCheek);\n              const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n              let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n              yawSignal = Math.tanh(yawSignal);\n              medianPush(bufYaw.current, yawSignal, 5);\n              yawTarget.current = median(bufYaw.current);\n\n              setUiFaceActive(true);\n            } else {\n              setUiFaceActive(false);\n            }\n          } catch {\n            setUiFaceActive(false);\n          }\n        }\n\n        if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n          (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n        } else {\n          requestAnimationFrame(detect);\n        }\n      };\n\n      requestAnimationFrame(detect);\n    };\n\n    start();\n\n    return () => {\n      stop = true;\n      const video = videoRef.current;\n      if (video) {\n        const stream = video.srcObject as MediaStream | null;\n        if (stream) stream.getTracks().forEach(t => t.stop());\n        video.pause();\n        video.srcObject = null;\n      }\n    };\n  }, [enableWebcam, modelsReady, depthResponse]);\n\n  return (\n    <div ref={containerRef} className={`gridscan${className ? ` ${className}` : ''}`} style={style}>\n      {showPreview && (\n        <div className=\"gridscan__preview\">\n          <video ref={videoRef} muted playsInline autoPlay className=\"gridscan__video\" />\n          <div className=\"gridscan__badge\">\n            {enableWebcam\n              ? modelsReady\n                ? uiFaceActive\n                  ? 'Face: tracking'\n                  : 'Face: searching'\n                : 'Loading models'\n              : 'Webcam disabled'}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nfunction srgbColor(hex: string) {\n  const c = new THREE.Color(hex);\n  return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n  current: THREE.Vector2,\n  target: THREE.Vector2,\n  currentVelocity: THREE.Vector2,\n  smoothTime: number,\n  maxSpeed: number,\n  deltaTime: number\n): THREE.Vector2 {\n  const out = current.clone();\n  smoothTime = Math.max(0.0001, smoothTime);\n  const omega = 2 / smoothTime;\n  const x = omega * deltaTime;\n  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n  let change = current.clone().sub(target);\n  const originalTo = target.clone();\n\n  const maxChange = maxSpeed * smoothTime;\n  if (change.length() > maxChange) change.setLength(maxChange);\n\n  target = current.clone().sub(change);\n  const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n  currentVelocity.sub(temp.clone().multiplyScalar(omega));\n  currentVelocity.multiplyScalar(exp);\n\n  out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n  const origMinusCurrent = originalTo.clone().sub(current);\n  const outMinusOrig = out.clone().sub(originalTo);\n  if (origMinusCurrent.dot(outMinusOrig) > 0) {\n    out.copy(originalTo);\n    currentVelocity.set(0, 0);\n  }\n  return out;\n}\n\nfunction smoothDampFloat(\n  current: number,\n  target: number,\n  velRef: { v: number },\n  smoothTime: number,\n  maxSpeed: number,\n  deltaTime: number\n): { value: number; v: number } {\n  smoothTime = Math.max(0.0001, smoothTime);\n  const omega = 2 / smoothTime;\n  const x = omega * deltaTime;\n  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n  let change = current - target;\n  const originalTo = target;\n\n  const maxChange = maxSpeed * smoothTime;\n  change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n  target = current - change;\n  const temp = (velRef.v + omega * change) * deltaTime;\n  velRef.v = (velRef.v - omega * temp) * exp;\n\n  let out = target + (change + temp) * exp;\n\n  const origMinusCurrent = originalTo - current;\n  const outMinusOrig = out - originalTo;\n  if (origMinusCurrent * outMinusOrig > 0) {\n    out = originalTo;\n    velRef.v = 0;\n  }\n  return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n  buf.push(v);\n  if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n  if (buf.length === 0) return 0;\n  const a = [...buf].sort((x, y) => x - y);\n  const mid = Math.floor(a.length / 2);\n  return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n  let x = 0,\n    y = 0;\n  const n = points.length || 1;\n  for (const p of points) {\n    x += p.x;\n    y += p.y;\n  }\n  return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n  return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"face-api.js@^0.22.2",
		"postprocessing@^6.36.0",
		"three@^0.180.0"
	]
}