{"$schema":"https://ui.shadcn.com/schema/registry.json","homepage":"https://uicapsule.com/ui/wireframe-orb","name":"wireframe-orb","type":"registry:block","author":"Kaiyu Hsu <uicapsule@kyh.io>","dependencies":["@react-three/drei","@react-three/fiber","@react-three/postprocessing","postprocessing","three"],"devDependencies":["@types/three"],"registryDependencies":[],"files":[{"type":"registry:file","path":"/package.json","content":"{\n  \"name\": \"@uicapsule/wireframe-orb\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./preview\": \"./preview.tsx\"\n  },\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .cache .turbo dist node_modules\"\n  },\n  \"dependencies\": {\n    \"@react-three/drei\": \"^10.7.8\",\n    \"@react-three/fiber\": \"^9.7.0\",\n    \"@react-three/postprocessing\": \"^3.1.1\",\n    \"postprocessing\": \"^6.39.4\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"three\": \"^0.185.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@types/three\": \"^0.185.4\"\n  }\n}\n","target":"uicapsule/wireframe-orb/package.json"},{"type":"registry:file","path":"/preview.tsx","content":"\"use client\";\n\nimport { WireframeOrb } from \"./wireframe-orb\";\n\nconst Preview = () => {\n  return (\n    <div className=\"h-screen w-full\">\n      <WireframeOrb />\n    </div>\n  );\n};\n\nexport default Preview;\n","target":"uicapsule/wireframe-orb/preview.tsx"},{"type":"registry:file","path":"/wireframe-orb.tsx","content":"\"use client\";\n\nimport { useRef, useMemo, useEffect } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { OrbitControls } from \"@react-three/drei\";\nimport { EffectComposer, Bloom } from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\n\n/**\n * Configuration options for the wireframe orb.\n * All fields are optional and fall back to sensible defaults.\n */\nexport type WireframeOrbConfig = {\n  /** CSS color string for the lines. @default \"#c0ebfc\" */\n  color?: string;\n  /** CSS color string for the canvas background. @default \"#0a0a0a\" */\n  background?: string;\n  /** Animation speed multiplier. @default 20 */\n  speed?: number;\n  /** Grid resolution per side. Total vertices = gridSize². Consider 150–200 on mobile. @default 200 */\n  gridSize?: number;\n  /** Curl noise density — higher values produce tighter noise. @default 0.7 */\n  noiseDensity?: number;\n  /** Scale of the noise displacement in world units. @default 3.0 */\n  noiseScale?: number;\n  /** Minimum line alpha in the pulsing animation. @default 0.01 */\n  minAlpha?: number;\n  /** Maximum line alpha in the pulsing animation. @default 0.45 */\n  maxAlpha?: number;\n  /** Bloom post-processing intensity. Set to 0 to disable. @default 1.5 */\n  bloomIntensity?: number;\n  /** Bloom luminance threshold — pixels brighter than this glow. @default 0.0 */\n  bloomThreshold?: number;\n  /** Bloom blur radius in pixels. @default 0.85 */\n  bloomRadius?: number;\n  /** Whether scroll-to-zoom is enabled. @default true */\n  enableZoom?: boolean;\n  /** Whether click-and-drag panning is enabled. @default false */\n  enablePan?: boolean;\n  /** Minimum camera distance (closest zoom). @default 2 */\n  minDistance?: number;\n  /** Maximum camera distance (farthest zoom). @default 20 */\n  maxDistance?: number;\n};\n\nconst defaults: Required<WireframeOrbConfig> = {\n  color: \"#c0ebfc\",\n  background: \"#0a0a0a\",\n  speed: 20,\n  gridSize: 200,\n  noiseDensity: 0.7,\n  noiseScale: 3.0,\n  minAlpha: 0.01,\n  maxAlpha: 0.45,\n  bloomIntensity: 1.5,\n  bloomThreshold: 0.0,\n  bloomRadius: 0.85,\n  enableZoom: true,\n  enablePan: false,\n  minDistance: 2,\n  maxDistance: 20,\n};\n\n/**\n * GLSL vertex shader for the wireframe orb.\n *\n * Takes a 2D UV grid attribute (`aUv`) and displaces each vertex in 3D space\n * using curl noise derived from simplex noise. The result is an organic,\n * continuously flowing cloud of connected line segments.\n */\nconst vertexShader = /* glsl */ `\n  attribute vec2 aUv;\n\n  uniform float time;\n  uniform float uSpeed;\n  uniform float uDensity;\n  uniform float uScale;\n\n  varying vec2 vUv;\n  varying float vPositionZ;\n\n  // --- Simplex noise (Ashima Arts, MIT License) ---\n  // https://github.com/ashima/webgl-noise\n\n  vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\n  vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\n  vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\n  vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\n  float snoise(vec3 v) {\n    const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);\n    const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\n    vec3 i  = floor(v + dot(v, C.yyy));\n    vec3 x0 = v - i + dot(i, C.xxx);\n\n    vec3 g = step(x0.yzx, x0.xyz);\n    vec3 l = 1.0 - g;\n    vec3 i1 = min(g.xyz, l.zxy);\n    vec3 i2 = max(g.xyz, l.zxy);\n\n    vec3 x1 = x0 - i1 + C.xxx;\n    vec3 x2 = x0 - i2 + C.yyy;\n    vec3 x3 = x0 - D.yyy;\n\n    i = mod289(i);\n    vec4 p = permute(permute(permute(\n      i.z + vec4(0.0, i1.z, i2.z, 1.0))\n      + i.y + vec4(0.0, i1.y, i2.y, 1.0))\n      + i.x + vec4(0.0, i1.x, i2.x, 1.0));\n\n    float n_ = 0.142857142857;\n    vec3 ns = n_ * D.wyz - D.xzx;\n    vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n    vec4 x_ = floor(j * ns.z);\n    vec4 y_ = floor(j - 7.0 * x_);\n    vec4 x = x_ * ns.x + ns.yyyy;\n    vec4 y = y_ * ns.x + ns.yyyy;\n    vec4 h = 1.0 - abs(x) - abs(y);\n    vec4 b0 = vec4(x.xy, y.xy);\n    vec4 b1 = vec4(x.zw, y.zw);\n    vec4 s0 = floor(b0) * 2.0 + 1.0;\n    vec4 s1 = floor(b1) * 2.0 + 1.0;\n    vec4 sh = -step(h, vec4(0.0));\n    vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n    vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n    vec3 p0 = vec3(a0.xy, h.x);\n    vec3 p1 = vec3(a0.zw, h.y);\n    vec3 p2 = vec3(a1.xy, h.z);\n    vec3 p3 = vec3(a1.zw, h.w);\n\n    vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\n    p0 *= norm.x;\n    p1 *= norm.y;\n    p2 *= norm.z;\n    p3 *= norm.w;\n\n    vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);\n    m = m * m;\n    return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\n  }\n\n  // --- Curl noise (derived from simplex noise) ---\n  // https://www.npmjs.com/package/glsl-curl-noise\n\n  vec3 snoiseVec3(vec3 x) {\n    return vec3(\n      snoise(vec3(x)),\n      snoise(vec3(x.y - 19.1, x.z + 33.4, x.x + 47.2)),\n      snoise(vec3(x.z + 74.2, x.x - 124.5, x.y + 99.4))\n    );\n  }\n\n  vec3 curlNoise(vec3 p) {\n    const float e = 0.1;\n    vec3 dx = vec3(e, 0.0, 0.0);\n    vec3 dy = vec3(0.0, e, 0.0);\n    vec3 dz = vec3(0.0, 0.0, e);\n\n    vec3 p_x0 = snoiseVec3(p - dx);\n    vec3 p_x1 = snoiseVec3(p + dx);\n    vec3 p_y0 = snoiseVec3(p - dy);\n    vec3 p_y1 = snoiseVec3(p + dy);\n    vec3 p_z0 = snoiseVec3(p - dz);\n    vec3 p_z1 = snoiseVec3(p + dz);\n\n    float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y;\n    float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z;\n    float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x;\n\n    const float divisor = 1.0 / (2.0 * e);\n    return normalize(vec3(x, y, z) * divisor);\n  }\n\n  void main() {\n    vUv = aUv;\n    vec3 pos = vec3(aUv * 2.0 - 1.0, 0.0) + time * uSpeed;\n    vec3 noise = curlNoise(pos * uDensity);\n    pos = noise * uScale;\n    vPositionZ = noise.z;\n    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);\n  }\n`;\n\n/**\n * GLSL fragment shader for the wireframe orb.\n *\n * Produces a pulsing alpha effect modulated by depth (curl noise z-component).\n */\nconst fragmentShader = /* glsl */ `\n  uniform float time;\n  uniform vec3 uColor;\n  uniform float uMinAlpha;\n  uniform float uMaxAlpha;\n  uniform float uAlphaSpeed;\n\n  varying vec2 vUv;\n  varying float vPositionZ;\n\n  const float PI2 = 6.2831853;\n\n  void main() {\n    float cAlpha = mix(uMinAlpha, uMaxAlpha, (sin(vUv.x * PI2 + time * uAlphaSpeed) + 1.0) * 0.5);\n    cAlpha *= mix(0.8, 1.0, vPositionZ);\n    gl_FragColor = vec4(uColor, cAlpha);\n  }\n`;\n\n/** Detect low-end devices for adaptive grid sizing. */\nfunction getAdaptiveGridSize(requested: number): number {\n  if (typeof navigator === \"undefined\") return requested;\n  const cores = navigator.hardwareConcurrency ?? 4;\n  if (cores <= 4 || window.devicePixelRatio >= 3) {\n    return Math.min(requested, 150);\n  }\n  return requested;\n}\n\n/** Internal scene component for the wireframe line strip. */\nfunction WireframeScene({ config }: { config: Required<WireframeOrbConfig> }) {\n  const materialRef = useRef<THREE.ShaderMaterial>(null);\n\n  const geometry = useMemo(() => {\n    const n = getAdaptiveGridSize(config.gridSize);\n    const maxI = n - 1;\n    const vertexCount = n * n;\n    const uvs = new Float32Array(vertexCount * 2);\n    // The vertex shader derives every position from `aUv`, so `position` only has to\n    // exist (zero-filled) for three.js to infer the draw range for the line strip.\n    const positions = new Float32Array(vertexCount * 3);\n\n    for (let j = 0; j < n; j++) {\n      for (let i = 0; i < n; i++) {\n        const v = (j * n + i) * 2;\n        uvs[v] = i / maxI;\n        uvs[v + 1] = 1 - j / maxI;\n      }\n    }\n\n    const geo = new THREE.BufferGeometry();\n    geo.setAttribute(\"position\", new THREE.Float32BufferAttribute(positions, 3));\n    geo.setAttribute(\"aUv\", new THREE.Float32BufferAttribute(uvs, 2));\n    return geo;\n  }, [config.gridSize]);\n\n  useEffect(() => {\n    return () => geometry.dispose();\n  }, [geometry]);\n\n  const uniforms = useMemo(() => {\n    const col = new THREE.Color(config.color);\n    return {\n      time: { value: 0 },\n      uSpeed: { value: config.speed * 0.005 },\n      uDensity: { value: config.noiseDensity },\n      uScale: { value: config.noiseScale },\n      uColor: { value: col },\n      uMinAlpha: { value: config.minAlpha },\n      uMaxAlpha: { value: config.maxAlpha },\n      uAlphaSpeed: { value: config.speed * 0.025 },\n    };\n  }, [config]);\n\n  useFrame((state) => {\n    if (!materialRef.current) return;\n    const timeUniform = materialRef.current.uniforms.time;\n    if (!timeUniform) return;\n    timeUniform.value = state.clock.elapsedTime;\n  });\n\n  return (\n    // @ts-expect-error R3F's <line> conflicts with SVG <line> in JSX\n    <line geometry={geometry}>\n      <shaderMaterial\n        ref={materialRef}\n        vertexShader={vertexShader}\n        fragmentShader={fragmentShader}\n        uniforms={uniforms}\n        transparent\n        depthWrite={false}\n        blending={THREE.AdditiveBlending}\n      />\n    </line>\n  );\n}\n\n/**\n * Curl-noise-displaced particle cloud rendered as a continuous line strip\n * with pulsing alpha and a bloom post-processing glow.\n *\n * @example\n * ```tsx\n * <WireframeOrb />\n * <WireframeOrb config={{ color: \"#ff66aa\", bloomIntensity: 2.0 }} />\n * ```\n */\nexport function WireframeOrb({\n  config: configOverrides,\n  className = \"\",\n}: {\n  config?: WireframeOrbConfig;\n  className?: string;\n}) {\n  const configKey = JSON.stringify(configOverrides);\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const config = useMemo(() => ({ ...defaults, ...configOverrides }), [configKey]);\n\n  return (\n    <div className={`w-full h-full ${className}`} style={{ background: config.background }}>\n      <Canvas camera={{ position: [0, 0, 12], fov: 45 }} gl={{ antialias: true, alpha: false }}>\n        <color attach=\"background\" args={[config.background]} />\n        <WireframeScene config={config} />\n        {config.bloomIntensity > 0 && (\n          <EffectComposer>\n            <Bloom\n              intensity={config.bloomIntensity}\n              luminanceThreshold={config.bloomThreshold}\n              radius={config.bloomRadius}\n            />\n          </EffectComposer>\n        )}\n        <OrbitControls\n          enablePan={config.enablePan}\n          enableZoom={config.enableZoom}\n          minDistance={config.minDistance}\n          maxDistance={config.maxDistance}\n        />\n      </Canvas>\n    </div>\n  );\n}\n","target":"uicapsule/wireframe-orb/wireframe-orb.tsx"}]}