{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"globe-wireframe","type":"registry:ui","title":"Globe Wireframe","description":"A rotating wireframe globe of latitude and longitude lines with depth-faded hidden lines and a glowing scan that sweeps pole to pole or across the face. Drag to spin.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/globe-wireframe.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface GlobeWireframeProps {\n  /** Grid line color. */\n  lineColor?: string;\n  /** Scan highlight and rim color. */\n  accent?: string;\n  /** Degrees between parallels. */\n  latStep?: number;\n  /** Degrees between meridians. */\n  lonStep?: number;\n  /** Auto-rotation, degrees per second. 0 stops it. */\n  speed?: number;\n  /** Axial tilt in degrees. */\n  tilt?: number;\n  /** \"latitude\" sweeps a ring pole to pole; \"longitude\" sweeps a band across the face. */\n  scan?: \"latitude\" | \"longitude\" | \"none\";\n  /** Seconds for one scan pass. */\n  scanDuration?: number;\n  className?: string;\n}\n\nconst RAD = Math.PI / 180;\n\nfunction hexToRgb(hex: string) {\n  const n = parseInt(hex.replace(\"#\", \"\").padEnd(6, \"0\").slice(0, 6), 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n}\n\nexport function GlobeWireframe({\n  lineColor = \"#64748b\",\n  accent = \"#22d3ee\",\n  latStep = 15,\n  lonStep = 15,\n  speed = 10,\n  tilt = 20,\n  scan = \"latitude\",\n  scanDuration = 4,\n  className,\n}: GlobeWireframeProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const rot = useRef({ yaw: 0, pitch: 0 });\n\n  useEffect(() => {\n    const canvas = ref.current!;\n    const ctx = canvas.getContext(\"2d\")!;\n    const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const line = hexToRgb(lineColor);\n    const acc = hexToRgb(accent);\n\n    // Polylines of [x, y, z, lat] on the unit sphere.\n    const lines: number[][] = [];\n    const ls = Math.max(5, latStep);\n    const ms = Math.max(5, lonStep);\n    for (let lat = -90 + ls; lat < 90 - 1e-6; lat += ls) {\n      const p: number[] = [];\n      for (let lon = 0; lon <= 360; lon += 4) p.push(Math.cos(lat * RAD) * Math.sin(lon * RAD), Math.sin(lat * RAD), Math.cos(lat * RAD) * Math.cos(lon * RAD), lat);\n      lines.push(p);\n    }\n    for (let lon = 0; lon < 360; lon += ms) {\n      const p: number[] = [];\n      for (let lat = -90; lat <= 90; lat += 3) p.push(Math.cos(lat * RAD) * Math.sin(lon * RAD), Math.sin(lat * RAD), Math.cos(lat * RAD) * Math.cos(lon * RAD), lat);\n      lines.push(p);\n    }\n\n    let w = 0;\n    let h = 0;\n    let R = 0;\n    let raf = 0;\n    let visible = true;\n    let last = performance.now();\n    let vel = 0;\n    const drag = { on: false, x: 0, y: 0, t: 0 };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = canvas.offsetWidth;\n      h = canvas.offsetHeight;\n      canvas.width = w * dpr;\n      canvas.height = h * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      R = Math.min(w, h) * 0.4;\n      draw(last);\n    };\n\n    const draw = (now: number) => {\n      const yaw = rot.current.yaw;\n      const pitch = tilt * RAD + rot.current.pitch;\n      const cY = Math.cos(yaw);\n      const sY = Math.sin(yaw);\n      const cT = Math.cos(pitch);\n      const sT = Math.sin(pitch);\n      const cx = w / 2;\n      const cy = h / 2;\n      ctx.clearRect(0, 0, w, h);\n\n      // eased ping-pong 0..1..0 for the scan position\n      const ph = reduce ? 0.35 : (now / 1000 / Math.max(0.5, scanDuration)) % 2;\n      const tri = ph < 1 ? ph : 2 - ph;\n      const eased = tri * tri * (3 - 2 * tri);\n      const scanLat = -80 + eased * 160;\n      const scanLon = (-95 + eased * 190) * RAD; // view space\n      const band = scan === \"latitude\" ? 14 : 0.3;\n\n      // halo + rim\n      const halo = ctx.createRadialGradient(cx, cy, R * 0.95, cx, cy, R * 1.22);\n      halo.addColorStop(0, `rgba(${acc},0.16)`);\n      halo.addColorStop(1, `rgba(${acc},0)`);\n      ctx.fillStyle = halo;\n      ctx.fillRect(0, 0, w, h);\n      const body = ctx.createRadialGradient(cx - R * 0.3, cy - R * 0.35, 0, cx, cy, R);\n      body.addColorStop(0, `rgba(${acc},0.07)`);\n      body.addColorStop(1, `rgba(${acc},0.015)`);\n      ctx.fillStyle = body;\n      ctx.beginPath();\n      ctx.arc(cx, cy, R, 0, Math.PI * 2);\n      ctx.fill();\n\n      // grid, batched into depth buckets; scan-lit segments collected for a second pass\n      const buckets: Path2D[] = [new Path2D(), new Path2D(), new Path2D(), new Path2D()];\n      const lit: [number, number, number, number, number][] = [];\n      for (const p of lines) {\n        let px = 0;\n        let py = 0;\n        let pz = 0;\n        let pl = 0;\n        for (let i = 0; i < p.length; i += 4) {\n          const x1 = p[i] * cY + p[i + 2] * sY;\n          const z1 = -p[i] * sY + p[i + 2] * cY;\n          const y2 = p[i + 1] * cT - z1 * sT;\n          const z2 = p[i + 1] * sT + z1 * cT;\n          const sx = cx + x1 * R;\n          const sy = cy - y2 * R;\n          const vl = Math.atan2(x1, z2);\n          if (i > 0) {\n            const z = (z2 + pz) / 2;\n            const b = z < -0.05 ? 0 : z < 0.35 ? 1 : z < 0.7 ? 2 : 3;\n            buckets[b].moveTo(px, py);\n            buckets[b].lineTo(sx, sy);\n            if (scan !== \"none\") {\n              const d = scan === \"latitude\" ? Math.abs((p[i + 3] + pl) / 2 - scanLat) : Math.abs(vl - scanLon);\n              const k = Math.exp(-((d / band) ** 2));\n              if (k > 0.04 && (scan === \"latitude\" || z2 > 0)) lit.push([px, py, sx, sy, k * (z > 0 ? 1 : 0.25)]);\n            }\n          }\n          px = sx;\n          py = sy;\n          pz = z2;\n          pl = p[i + 3];\n        }\n      }\n      ctx.lineCap = \"round\";\n      ctx.lineWidth = 1;\n      [0.07, 0.22, 0.4, 0.6].forEach((a, i) => {\n        ctx.strokeStyle = `rgba(${line},${a})`;\n        ctx.stroke(buckets[i]);\n      });\n      for (const [x0, y0, x1, y1, k] of lit) {\n        ctx.strokeStyle = `rgba(${acc},${k * 0.25})`;\n        ctx.lineWidth = 5;\n        ctx.beginPath();\n        ctx.moveTo(x0, y0);\n        ctx.lineTo(x1, y1);\n        ctx.stroke();\n        ctx.strokeStyle = `rgba(${acc},${Math.min(1, k * 1.1)})`;\n        ctx.lineWidth = 1.4;\n        ctx.stroke();\n      }\n\n      // the scan ring itself\n      if (scan !== \"none\") {\n        const ring = new Path2D();\n        const back = new Path2D();\n        let first = true;\n        let prevFront = true;\n        for (let a = 0; a <= 360; a += 3) {\n          let x: number;\n          let y: number;\n          let z: number;\n          if (scan === \"latitude\") {\n            const c = Math.cos(scanLat * RAD);\n            const vx = c * Math.sin(a * RAD);\n            const vy = Math.sin(scanLat * RAD);\n            const vz = c * Math.cos(a * RAD);\n            x = vx; // yaw does not move a parallel\n            y = vy * cT - vz * sT;\n            z = vy * sT + vz * cT;\n          } else {\n            const phi = (a / 360) * Math.PI - Math.PI / 2;\n            x = Math.cos(phi) * Math.sin(scanLon);\n            y = Math.sin(phi);\n            z = Math.cos(phi) * Math.cos(scanLon);\n            if (a > 180) break;\n          }\n          const sx = cx + x * R;\n          const sy = cy - y * R;\n          const front = z >= 0;\n          const path = front ? ring : back;\n          if (first || front !== prevFront) path.moveTo(sx, sy);\n          else path.lineTo(sx, sy);\n          first = false;\n          prevFront = front;\n        }\n        ctx.strokeStyle = `rgba(${acc},0.2)`;\n        ctx.lineWidth = 1;\n        ctx.stroke(back);\n        ctx.strokeStyle = `rgba(${acc},0.3)`;\n        ctx.lineWidth = 7;\n        ctx.stroke(ring);\n        ctx.strokeStyle = `rgba(${acc},0.95)`;\n        ctx.lineWidth = 1.8;\n        ctx.stroke(ring);\n      }\n\n      ctx.strokeStyle = `rgba(${acc},0.35)`;\n      ctx.lineWidth = 1.2;\n      ctx.beginPath();\n      ctx.arc(cx, cy, R, 0, Math.PI * 2);\n      ctx.stroke();\n    };\n\n    const loop = (now: number) => {\n      const dt = Math.min(0.05, (now - last) / 1000);\n      last = now;\n      if (!drag.on) {\n        rot.current.yaw += vel * dt + (reduce ? 0 : speed * RAD * dt);\n        vel *= Math.pow(0.04, dt);\n        rot.current.pitch *= Math.pow(0.2, dt);\n      }\n      draw(now);\n      raf = visible && (!reduce || drag.on || Math.abs(vel) > 0.01 || Math.abs(rot.current.pitch) > 0.001) ? requestAnimationFrame(loop) : 0;\n    };\n    const kick = () => {\n      if (!raf && visible) {\n        last = performance.now();\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const down = (e: PointerEvent) => {\n      drag.on = true;\n      drag.x = e.clientX;\n      drag.y = e.clientY;\n      drag.t = performance.now();\n      vel = 0;\n      canvas.setPointerCapture(e.pointerId);\n      canvas.style.cursor = \"grabbing\";\n    };\n    const move = (e: PointerEvent) => {\n      if (!drag.on) return;\n      const now = performance.now();\n      const d = (e.clientX - drag.x) / R;\n      rot.current.yaw += d;\n      rot.current.pitch = Math.max(-0.6, Math.min(0.6, rot.current.pitch + (e.clientY - drag.y) / R));\n      vel = (d / Math.max(1, now - drag.t)) * 1000;\n      drag.x = e.clientX;\n      drag.y = e.clientY;\n      drag.t = now;\n      kick();\n    };\n    const up = () => {\n      drag.on = false;\n      canvas.style.cursor = \"grab\";\n      kick();\n    };\n    const key = (e: KeyboardEvent) => {\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n        e.preventDefault();\n        vel += (e.key === \"ArrowLeft\" ? -1 : 1) * 1.6;\n        kick();\n      }\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting;\n      if (visible) kick();\n    });\n    io.observe(canvas);\n    canvas.addEventListener(\"pointerdown\", down);\n    canvas.addEventListener(\"pointermove\", move);\n    canvas.addEventListener(\"pointerup\", up);\n    canvas.addEventListener(\"pointercancel\", up);\n    canvas.addEventListener(\"keydown\", key);\n    kick();\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      canvas.removeEventListener(\"pointerdown\", down);\n      canvas.removeEventListener(\"pointermove\", move);\n      canvas.removeEventListener(\"pointerup\", up);\n      canvas.removeEventListener(\"pointercancel\", up);\n      canvas.removeEventListener(\"keydown\", key);\n    };\n  }, [lineColor, accent, latStep, lonStep, speed, tilt, scan, scanDuration]);\n\n  return (\n    <canvas\n      ref={ref}\n      tabIndex={0}\n      role=\"img\"\n      aria-label=\"Rotating wireframe globe. Drag or use arrow keys to spin.\"\n      className={cn(\"aspect-square w-full max-w-[520px] cursor-grab touch-none rounded-3xl outline-none focus-visible:ring-2 focus-visible:ring-ring/50\", className)}\n    />\n  );\n}\n"}]}