{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GhostCursor-TS-TW",
	"title": "GhostCursor",
	"description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "GhostCursor/GhostCursor.tsx",
			"content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\n\ntype GhostCursorProps = {\n  className?: string;\n  style?: React.CSSProperties;\n\n  trailLength?: number;\n  inertia?: number;\n  grainIntensity?: number;\n  bloomStrength?: number;\n  bloomRadius?: number;\n  bloomThreshold?: number;\n\n  brightness?: number;\n  color?: string;\n  mixBlendMode?: React.CSSProperties['mixBlendMode'];\n  edgeIntensity?: number;\n\n  maxDevicePixelRatio?: number;\n  targetPixels?: number;\n  fadeDelayMs?: number;\n  fadeDurationMs?: number;\n  zIndex?: number;\n};\n\nconst GhostCursor: React.FC<GhostCursorProps> = ({\n  className,\n  style,\n  trailLength = 50,\n  inertia = 0.5,\n  grainIntensity = 0.05,\n  bloomStrength = 0.1,\n  bloomRadius = 1.0,\n  bloomThreshold = 0.025,\n\n  brightness = 1,\n  color = '#B497CF',\n  mixBlendMode = 'screen',\n  edgeIntensity = 0,\n\n  maxDevicePixelRatio = 0.5,\n  targetPixels,\n\n  fadeDelayMs,\n  fadeDurationMs,\n  zIndex = 10\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);\n  const composerRef = useRef<EffectComposer | null>(null);\n  const materialRef = useRef<THREE.ShaderMaterial | null>(null);\n  const bloomPassRef = useRef<UnrealBloomPass | null>(null);\n  const filmPassRef = useRef<ShaderPass | null>(null);\n\n  // Trail circular buffer\n  const trailBufRef = useRef<THREE.Vector2[]>([]);\n  const headRef = useRef(0);\n\n  const rafRef = useRef<number | null>(null);\n  const resizeObsRef = useRef<ResizeObserver | null>(null);\n  const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n  const velocityRef = useRef(new THREE.Vector2(0, 0));\n  const fadeOpacityRef = useRef(1.0);\n  const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n  const pointerActiveRef = useRef(false);\n  const runningRef = useRef(false);\n  const hasValidSizeRef = useRef(false);\n\n  const isTouch = useMemo(\n    () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n    []\n  );\n\n  const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n  const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n  const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n  const baseVertexShader = `\n    varying vec2 vUv;\n    void main() {\n      vUv = uv;\n      gl_Position = vec4(position, 1.0);\n    }\n  `;\n\n  const fragmentShader = `\n    uniform float iTime;\n    uniform vec3  iResolution;\n    uniform vec2  iMouse;\n    uniform vec2  iPrevMouse[MAX_TRAIL_LENGTH];\n    uniform float iOpacity;\n    uniform float iScale;\n    uniform vec3  iBaseColor;\n    uniform float iBrightness;\n    uniform float iEdgeIntensity;\n    varying vec2  vUv;\n\n    float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n    float noise(vec2 p){\n      vec2 i = floor(p), f = fract(p);\n      f *= f * (3. - 2. * f);\n      return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n                 mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n    }\n    float fbm(vec2 p){\n      float v = 0.0;\n      float a = 0.5;\n      mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n      for(int i=0;i<5;i++){\n        v += a * noise(p);\n        p = m * p * 2.0;\n        a *= 0.5;\n      }\n      return v;\n    }\n    vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n    vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n    vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n      vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n      vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n      float smoke = fbm(p * iScale + r * 0.8);\n      float radius = 0.5 + 0.3 * (1.0 / iScale);\n      float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n      float alpha = pow(smoke, 2.5) * distFactor;\n\n      vec3 c1 = tint1(iBaseColor);\n      vec3 c2 = tint2(iBaseColor);\n      vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n      return vec4(color * alpha * intensity, alpha * intensity);\n    }\n\n    void main() {\n      vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n      vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n      vec3 colorAcc = vec3(0.0);\n      float alphaAcc = 0.0;\n\n      vec4 b = blob(uv, mouse, 1.0, iOpacity);\n      colorAcc += b.rgb;\n      alphaAcc += b.a;\n\n      for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n        vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n        float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n        t = pow(t, 2.0);\n        if (t > 0.01) {\n          vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n          colorAcc += bt.rgb;\n          alphaAcc += bt.a;\n        }\n      }\n\n      colorAcc *= iBrightness;\n\n      vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n      float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n      float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n      float k = clamp(iEdgeIntensity, 0.0, 1.0);\n      float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n      float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n      gl_FragColor = vec4(colorAcc, outAlpha);\n    }\n  `;\n\n  const FilmGrainShader = useMemo(() => {\n    return {\n      uniforms: {\n        tDiffuse: { value: null },\n        iTime: { value: 0 },\n        intensity: { value: grainIntensity }\n      },\n      vertexShader: `\n        varying vec2 vUv;\n        void main(){\n          vUv = uv;\n          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n        }\n      `,\n      fragmentShader: `\n        uniform sampler2D tDiffuse;\n        uniform float iTime;\n        uniform float intensity;\n        varying vec2 vUv;\n\n        float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n        void main(){\n          vec4 color = texture2D(tDiffuse, vUv);\n          float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n          color.rgb += n * intensity * color.rgb;\n          gl_FragColor = color;\n        }\n      `\n    };\n  }, [grainIntensity]);\n\n  const UnpremultiplyPass = useMemo(\n    () =>\n      new ShaderPass({\n        uniforms: { tDiffuse: { value: null } },\n        vertexShader: `\n          varying vec2 vUv;\n          void main(){\n            vUv = uv;\n            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n          }\n        `,\n        fragmentShader: `\n          uniform sampler2D tDiffuse;\n          varying vec2 vUv;\n          void main(){\n            vec4 c = texture2D(tDiffuse, vUv);\n            float coverage = clamp(max(c.r, max(c.g, c.b)), 0.0, 1.0);\n            vec3 straight = coverage > 1e-5 ? c.rgb / coverage : vec3(0.0);\n            gl_FragColor = vec4(clamp(straight, 0.0, 1.0), coverage);\n          }\n        `\n      }),\n    []\n  );\n\n  function calculateScale(el: HTMLElement) {\n    const r = el.getBoundingClientRect();\n    const base = 600;\n    const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n    return Math.max(0.5, Math.min(2.0, current / base));\n  }\n\n  useEffect(() => {\n    const host = containerRef.current;\n    const parent = host?.parentElement;\n    if (!host || !parent) return;\n\n    let active = true;\n\n    const prevParentPos = parent.style.position;\n    if (!prevParentPos || prevParentPos === 'static') {\n      parent.style.position = 'relative';\n    }\n\n    const renderer = new THREE.WebGLRenderer({\n      antialias: !isTouch,\n      alpha: true,\n      depth: false,\n      stencil: false,\n      powerPreference: isTouch ? 'low-power' : 'high-performance',\n      premultipliedAlpha: false,\n      preserveDrawingBuffer: false\n    });\n    renderer.setClearColor(0x000000, 0);\n    rendererRef.current = renderer;\n\n    renderer.domElement.style.pointerEvents = 'none';\n    if (mixBlendMode) {\n      renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n    } else {\n      renderer.domElement.style.removeProperty('mix-blend-mode');\n    }\n\n    renderer.domElement.style.display = 'block';\n    renderer.domElement.style.width = '100%';\n    renderer.domElement.style.height = '100%';\n    renderer.domElement.style.background = 'transparent';\n\n    host.appendChild(renderer.domElement);\n\n    const scene = new THREE.Scene();\n    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n    const geom = new THREE.PlaneGeometry(2, 2);\n\n    const maxTrail = Math.max(1, Math.floor(trailLength));\n    trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n    headRef.current = 0;\n\n    const baseColor = new THREE.Color(color);\n\n    const material = new THREE.ShaderMaterial({\n      defines: { MAX_TRAIL_LENGTH: maxTrail },\n      uniforms: {\n        iTime: { value: 0 },\n        iResolution: { value: new THREE.Vector3(1, 1, 1) },\n        iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n        iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n        iOpacity: { value: 1.0 },\n        iScale: { value: 1.0 },\n        iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n        iBrightness: { value: brightness },\n        iEdgeIntensity: { value: edgeIntensity }\n      },\n      vertexShader: baseVertexShader,\n      fragmentShader,\n      transparent: true,\n      depthTest: false,\n      depthWrite: false\n    });\n    materialRef.current = material;\n\n    const mesh = new THREE.Mesh(geom, material);\n    scene.add(mesh);\n\n    const composer = new EffectComposer(renderer);\n    composerRef.current = composer;\n\n    const renderPass = new RenderPass(scene, camera);\n    composer.addPass(renderPass);\n\n    const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n    bloomPassRef.current = bloomPass;\n    composer.addPass(bloomPass);\n\n    const filmPass = new ShaderPass(FilmGrainShader as any);\n    filmPassRef.current = filmPass;\n    composer.addPass(filmPass);\n\n    composer.addPass(UnpremultiplyPass);\n\n    const resize = () => {\n      if (!active) return;\n\n      const rect = host.getBoundingClientRect();\n      const cssW = Math.floor(rect.width);\n      const cssH = Math.floor(rect.height);\n\n      if (cssW <= 0 || cssH <= 0) {\n        hasValidSizeRef.current = false;\n        return;\n      }\n\n      const currentDPR = Math.min(\n        typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n        maxDevicePixelRatio\n      );\n      const need = cssW * cssH * currentDPR * currentDPR;\n      const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n      const pixelRatio = currentDPR * scale;\n\n      renderer.setPixelRatio(pixelRatio);\n      renderer.setSize(cssW, cssH, false);\n\n      composer.setPixelRatio?.(pixelRatio);\n      composer.setSize(cssW, cssH);\n\n      const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n      const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n      material.uniforms.iResolution.value.set(wpx, hpx, 1);\n      material.uniforms.iScale.value = calculateScale(host);\n      bloomPass.setSize(wpx, hpx);\n\n      hasValidSizeRef.current = true;\n    };\n\n    resize();\n    const ro = new ResizeObserver(resize);\n    resizeObsRef.current = ro;\n    ro.observe(parent);\n    ro.observe(host);\n\n    const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n    const animate = () => {\n      if (!active) return;\n\n      if (!hasValidSizeRef.current) {\n        rafRef.current = requestAnimationFrame(animate);\n        return;\n      }\n\n      const now = performance.now();\n      const t = (now - start) / 1000;\n\n      const mat = materialRef.current!;\n      const comp = composerRef.current!;\n\n      if (pointerActiveRef.current) {\n        velocityRef.current.set(\n          currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n          currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n        );\n        mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n        fadeOpacityRef.current = 1.0;\n      } else {\n        velocityRef.current.multiplyScalar(inertia);\n        if (velocityRef.current.lengthSq() > 1e-6) {\n          mat.uniforms.iMouse.value.add(velocityRef.current);\n        }\n        const dt = now - lastMoveTimeRef.current;\n        if (dt > fadeDelay) {\n          const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n          fadeOpacityRef.current = Math.max(0, 1 - k);\n        }\n      }\n\n      const N = trailBufRef.current.length;\n      headRef.current = (headRef.current + 1) % N;\n      trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n      const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n      for (let i = 0; i < N; i++) {\n        const srcIdx = (headRef.current - i + N) % N;\n        arr[i].copy(trailBufRef.current[srcIdx]);\n      }\n\n      mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n      mat.uniforms.iTime.value = t;\n\n      if (filmPassRef.current?.uniforms?.iTime) {\n        filmPassRef.current.uniforms.iTime.value = t;\n      }\n\n      comp.render();\n\n      if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n        runningRef.current = false;\n        rafRef.current = null;\n        return;\n      }\n\n      rafRef.current = requestAnimationFrame(animate);\n    };\n\n    const ensureLoop = () => {\n      if (!runningRef.current) {\n        runningRef.current = true;\n        rafRef.current = requestAnimationFrame(animate);\n      }\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = parent.getBoundingClientRect();\n      const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n      const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n      currentMouseRef.current.set(x, y);\n      pointerActiveRef.current = true;\n      lastMoveTimeRef.current = performance.now();\n      ensureLoop();\n    };\n    const onPointerEnter = () => {\n      pointerActiveRef.current = true;\n      ensureLoop();\n    };\n    const onPointerLeave = () => {\n      pointerActiveRef.current = false;\n      lastMoveTimeRef.current = performance.now();\n      ensureLoop();\n    };\n\n    parent.addEventListener('pointermove', onPointerMove, { passive: true });\n    parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n    parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n    ensureLoop();\n\n    return () => {\n      active = false;\n      hasValidSizeRef.current = false;\n\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      runningRef.current = false;\n      rafRef.current = null;\n\n      parent.removeEventListener('pointermove', onPointerMove);\n      parent.removeEventListener('pointerenter', onPointerEnter);\n      parent.removeEventListener('pointerleave', onPointerLeave);\n      resizeObsRef.current?.disconnect();\n\n      scene.clear();\n      geom.dispose();\n      material.dispose();\n      materialRef.current = null;\n      composer.dispose();\n      composerRef.current = null;\n      renderer.dispose();\n      renderer.forceContextLoss();\n      rendererRef.current = null;\n\n      if (renderer.domElement && renderer.domElement.parentElement) {\n        renderer.domElement.parentElement.removeChild(renderer.domElement);\n      }\n      if (!prevParentPos || prevParentPos === 'static') {\n        parent.style.position = prevParentPos;\n      }\n    };\n  }, [\n    trailLength,\n    inertia,\n    grainIntensity,\n    bloomStrength,\n    bloomRadius,\n    bloomThreshold,\n    pixelBudget,\n    fadeDelay,\n    fadeDuration,\n    isTouch,\n    color,\n    brightness,\n    mixBlendMode,\n    edgeIntensity\n  ]);\n\n  useEffect(() => {\n    if (materialRef.current) {\n      const c = new THREE.Color(color);\n      (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n    }\n  }, [color]);\n\n  useEffect(() => {\n    if (materialRef.current) {\n      materialRef.current.uniforms.iBrightness.value = brightness;\n    }\n  }, [brightness]);\n\n  useEffect(() => {\n    if (materialRef.current) {\n      materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n    }\n  }, [edgeIntensity]);\n\n  useEffect(() => {\n    if (filmPassRef.current?.uniforms?.intensity) {\n      filmPassRef.current.uniforms.intensity.value = grainIntensity;\n    }\n  }, [grainIntensity]);\n\n  useEffect(() => {\n    const el = rendererRef.current?.domElement;\n    if (!el) return;\n    if (mixBlendMode) {\n      el.style.mixBlendMode = String(mixBlendMode);\n    } else {\n      el.style.removeProperty('mix-blend-mode');\n    }\n  }, [mixBlendMode]);\n\n  const mergedStyle = useMemo<React.CSSProperties>(() => ({ zIndex, ...style }), [zIndex, style]);\n\n  return (\n    <div ref={containerRef} className={`pointer-events-none absolute inset-0 ${className ?? ''}`} style={mergedStyle} />\n  );\n};\n\nexport default GhostCursor;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"three@^0.180.0"
	]
}