{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wireframe-globe",
  "type": "registry:ui",
  "title": "Wireframe Globe",
  "description": "A rotating canvas globe with a depth-faded graticule, pulsing markers and travelling great-circle arcs.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/wireframe-globe.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface GlobeMarker {\n  /** Latitude in degrees (−90 south … 90 north). */\n  lat: number;\n  /** Longitude in degrees (−180 west … 180 east). */\n  lon: number;\n  /** Shown next to the dot and included in the accessible description. */\n  label?: string;\n  /** Optional id that arcs can reference. */\n  id?: string;\n}\n\nexport interface GlobeArc {\n  /** Marker index, id or label. */\n  from: number | string;\n  /** Marker index, id or label. */\n  to: number | string;\n}\n\nexport interface WireframeGlobeProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Pulsing points on the surface. Hoist or memoize the array to avoid re-initialising. */\n  markers?: GlobeMarker[];\n  /** Animated great-circle arcs between markers. */\n  arcs?: GlobeArc[];\n  /** Spin continuously around the polar axis. */\n  autoRotate?: boolean;\n  /** Auto-rotation speed multiplier (1 ≈ 9° per second). */\n  speed?: number;\n  /** Spacing of the latitude / longitude graticule, in degrees. */\n  gridStep?: number;\n  /** Initial tilt toward the viewer, in degrees (positive shows more of the north). */\n  tilt?: number;\n  /** Marker colour (any CSS colour). */\n  color?: string;\n  /** Arc colour (any CSS colour). */\n  arcColor?: string;\n  /** Show marker labels on the visible hemisphere. */\n  showLabels?: boolean;\n  /** Drag (mouse, pen, touch) and arrow keys rotate the globe. */\n  draggable?: boolean;\n  /** Globe radius as a fraction of the smaller container side. */\n  radius?: number;\n  /** Accessible name. Defaults to a summary of the markers and arcs. */\n  label?: string;\n}\n\ntype RGB = [number, number, number];\ntype Vec3 = [number, number, number];\n\n/* ---------------------------------- Colour --------------------------------- */\n\nlet probe: CanvasRenderingContext2D | null = null;\n\n/** Resolves any CSS colour (hex, rgb(), oklch()…) to RGB by painting it on a 1×1 canvas. */\nfunction resolveColor(input: string, fallback: RGB): RGB {\n  probe ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!probe) return fallback;\n  probe.clearRect(0, 0, 1, 1);\n  probe.fillStyle = \"rgba(0, 0, 0, 0)\";\n  probe.fillStyle = input; // invalid colours are ignored → transparent → fallback\n  probe.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;\n  return a === 0 ? fallback : [r, g, b];\n}\n\nconst luminance = ([r, g, b]: RGB) => (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;\n\n/** On light themes (dark ink), very light colours are pulled toward the ink so they stay visible. */\nfunction legible(color: RGB, ink: RGB): RGB {\n  if (luminance(ink) > 0.5 || luminance(color) < 0.62) return color;\n  return color.map((v, i) => Math.round(v + (ink[i] - v) * 0.42)) as RGB;\n}\n\nconst rgba = (c: RGB, a: number) => `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${Math.max(0, Math.min(1, a)).toFixed(3)})`;\n\n/** Re-reads colours when the theme changes: class/data-theme on <html> or a scoped [data-theme] ancestor. */\nfunction observeTheme(el: Element, onChange: () => void) {\n  const mo = new MutationObserver(onChange);\n  const options = { attributes: true, attributeFilter: [\"class\", \"data-theme\"] };\n  mo.observe(document.documentElement, options);\n  const scoped = el.parentElement?.closest(\"[data-theme]\");\n  if (scoped && scoped !== document.documentElement) mo.observe(scoped, options);\n  const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n  mq.addEventListener(\"change\", onChange);\n  return () => {\n    mo.disconnect();\n    mq.removeEventListener(\"change\", onChange);\n  };\n}\n\n/* ----------------------------- Reduced motion ------------------------------ */\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/* ---------------------------------- Maths ---------------------------------- */\n\nconst DEG = Math.PI / 180;\n// Stable empty defaults so the render effect does not re-run on every parent render.\nconst NO_MARKERS: GlobeMarker[] = [];\nconst NO_ARCS: GlobeArc[] = [];\nconst PULSE_MS = 2400;\nconst ARC_MS = 3600;\nconst KEY_IMPULSE = 0.0009; // rad/ms added per arrow key press (≈ 17° after inertia)\nconst KEY_STEP = 15 * DEG; // reduced motion: rotate instantly by this much\n\n/** Unit vector for a latitude/longitude pair (y up, z toward the viewer at lon 0). */\nfunction toVec(lat: number, lon: number): Vec3 {\n  const la = lat * DEG;\n  const lo = lon * DEG;\n  return [Math.cos(la) * Math.sin(lo), Math.sin(la), Math.cos(la) * Math.cos(lo)];\n}\n\nfunction findMarker(markers: GlobeMarker[], ref: number | string) {\n  if (typeof ref === \"number\") return markers[ref];\n  return markers.find((m) => m.id === ref) ?? markers.find((m) => m.label === ref);\n}\n\nfunction describe(markers: GlobeMarker[], arcs: GlobeArc[], draggable: boolean) {\n  const names = markers.map((m) => m.label).filter(Boolean);\n  let text = `Wireframe globe with ${markers.length} marker${markers.length === 1 ? \"\" : \"s\"}`;\n  if (names.length) text += `: ${names.join(\", \")}`;\n  if (arcs.length) text += `; ${arcs.length} connection${arcs.length === 1 ? \"\" : \"s\"}`;\n  text += \".\";\n  if (draggable) text += \" Use the arrow keys to rotate.\";\n  return text;\n}\n\n/**\n * A rotating wireframe globe on canvas 2D: depth-faded graticule, pulsing markers with labels\n * and animated great-circle arcs. Drag with inertia (pointer capture) or use the arrow keys\n * when focused. The loop pauses offscreen and in hidden tabs; reduced motion draws still frames\n * and rotates only in direct response to input.\n */\nexport function WireframeGlobe({\n  markers = NO_MARKERS,\n  arcs = NO_ARCS,\n  autoRotate = true,\n  speed = 1,\n  gridStep = 15,\n  tilt = 18,\n  color = \"#FF7448\",\n  arcColor = \"#B6F3D1\",\n  showLabels = true,\n  draggable = true,\n  radius = 0.4,\n  label,\n  className = \"\",\n  ...props\n}: WireframeGlobeProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  // Yaw survives prop changes so tweaking a setting does not snap the globe back.\n  const yawRef = React.useRef(60 * DEG);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!root || !canvas || !ctx) return;\n\n    const step = Math.max(5, Math.min(90, gridStep));\n    const view = { yaw: yawRef.current, pitch: Math.max(-70, Math.min(70, tilt)) * DEG, yawVel: 0, pitchVel: 0 };\n    const drag = { active: false, id: -1, x: 0, y: 0, t: 0 };\n    let focusVisible = false;\n\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let raf = 0;\n    let last = 0;\n    let clock = 0;\n    let inView = false;\n    let ink: RGB = [24, 24, 27];\n    let accent: RGB = [255, 116, 72];\n    let arcRgb: RGB = [182, 243, 209];\n    let font = \"10px ui-monospace, monospace\";\n\n    // Graticule polylines as unit vectors, built once per gridStep.\n    const graticule: Vec3[][] = [];\n    for (let lat = -90 + step; lat < 90; lat += step) {\n      const line: Vec3[] = [];\n      for (let lon = -180; lon <= 180; lon += 4) line.push(toVec(lat, lon));\n      graticule.push(line);\n    }\n    for (let lon = -180; lon < 180; lon += step) {\n      const line: Vec3[] = [];\n      for (let lat = -90; lat <= 90; lat += 4) line.push(toVec(lat, lon));\n      graticule.push(line);\n    }\n\n    const points = markers.map((m) => toVec(m.lat, m.lon));\n\n    // Great-circle arcs (slerp), lifted above the surface in proportion to their length.\n    const arcPaths: Vec3[][] = [];\n    for (const arc of arcs) {\n      const a = findMarker(markers, arc.from);\n      const b = findMarker(markers, arc.to);\n      if (!a || !b) continue;\n      const va = toVec(a.lat, a.lon);\n      const vb = toVec(b.lat, b.lon);\n      const dot = Math.max(-1, Math.min(1, va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2]));\n      const omega = Math.acos(dot);\n      if (omega < 1e-4) continue;\n      const sinO = Math.sin(omega);\n      const rise = 0.06 + 0.28 * (omega / Math.PI);\n      const path: Vec3[] = [];\n      const samples = 48;\n      for (let i = 0; i <= samples; i++) {\n        const t = i / samples;\n        const ka = Math.sin((1 - t) * omega) / sinO;\n        const kb = Math.sin(t * omega) / sinO;\n        const lift = 1 + rise * Math.sin(Math.PI * t);\n        path.push([(va[0] * ka + vb[0] * kb) * lift, (va[1] * ka + vb[1] * kb) * lift, (va[2] * ka + vb[2] * kb) * lift]);\n      }\n      arcPaths.push(path);\n    }\n\n    /** Rotates a world vector by yaw (around y) then pitch (around x). Returns view space. */\n    const rotate = (v: Vec3, cy: number, sy: number, cp: number, sp: number): Vec3 => {\n      const x = v[0] * cy - v[2] * sy;\n      const z1 = v[0] * sy + v[2] * cy;\n      return [x, v[1] * cp - z1 * sp, v[1] * sp + z1 * cp];\n    };\n\n    const readTheme = () => {\n      ink = resolveColor(getComputedStyle(canvas).color, [24, 24, 27]);\n      accent = legible(resolveColor(color, [255, 116, 72]), ink);\n      arcRgb = legible(resolveColor(arcColor, [182, 243, 209]), ink);\n      const mono = getComputedStyle(document.documentElement).getPropertyValue(\"--font-mono\").trim();\n      font = `500 10px ${mono || \"ui-monospace, SFMono-Regular, Menlo, monospace\"}`;\n    };\n\n    const draw = () => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, width, height);\n      const R = Math.min(width, height) * Math.max(0.1, Math.min(0.5, radius));\n      if (R <= 4) return;\n      const cx = width / 2;\n      const cy = height / 2;\n      const cyaw = Math.cos(view.yaw);\n      const syaw = Math.sin(view.yaw);\n      const cp = Math.cos(view.pitch);\n      const sp = Math.sin(view.pitch);\n      const animated = !reduceMotion;\n\n      // Disc + silhouette.\n      ctx.beginPath();\n      ctx.arc(cx, cy, R, 0, Math.PI * 2);\n      ctx.fillStyle = rgba(ink, 0.025);\n      ctx.fill();\n      ctx.lineWidth = 1;\n      ctx.strokeStyle = rgba(ink, 0.32);\n      ctx.stroke();\n\n      // Graticule split into depth bands: back, limb and front.\n      const back = new Path2D();\n      const limb = new Path2D();\n      const front = new Path2D();\n      for (const line of graticule) {\n        let prev = rotate(line[0], cyaw, syaw, cp, sp);\n        for (let i = 1; i < line.length; i++) {\n          const cur = rotate(line[i], cyaw, syaw, cp, sp);\n          const z = (prev[2] + cur[2]) / 2;\n          const path = z < 0 ? back : z < 0.35 ? limb : front;\n          path.moveTo(cx + prev[0] * R, cy - prev[1] * R);\n          path.lineTo(cx + cur[0] * R, cy - cur[1] * R);\n          prev = cur;\n        }\n      }\n      ctx.lineWidth = 0.75;\n      ctx.strokeStyle = rgba(ink, 0.08);\n      ctx.stroke(back);\n      ctx.strokeStyle = rgba(ink, 0.18);\n      ctx.stroke(limb);\n      ctx.strokeStyle = rgba(ink, 0.3);\n      ctx.stroke(front);\n\n      // Arcs: faint full route plus a travelling bright segment with a head dot.\n      ctx.lineCap = \"round\";\n      arcPaths.forEach((path, index) => {\n        const projected = path.map((v) => rotate(v, cyaw, syaw, cp, sp));\n        // A lifted point is visible in front of the globe or outside its silhouette.\n        const visible = projected.map((p) => p[2] > 0 || p[0] * p[0] + p[1] * p[1] > 1);\n        const stroke = (from: number, to: number) => {\n          ctx.beginPath();\n          let open = false;\n          for (let i = from; i <= to; i++) {\n            const p = projected[i];\n            if (!visible[i]) {\n              open = false;\n              continue;\n            }\n            if (!open) ctx.moveTo(cx + p[0] * R, cy - p[1] * R);\n            else ctx.lineTo(cx + p[0] * R, cy - p[1] * R);\n            open = true;\n          }\n          ctx.stroke();\n        };\n        const n = projected.length - 1;\n        ctx.lineWidth = 1;\n        ctx.strokeStyle = rgba(arcRgb, animated ? 0.22 : 0.7);\n        stroke(0, n);\n        if (!animated) return;\n        const phase = ((clock / ARC_MS + index * 0.29) % 1) * 1.5;\n        const head = Math.min(1, phase);\n        const tail = Math.max(0, phase - 0.5);\n        if (head <= tail) return;\n        ctx.lineWidth = 1.5;\n        ctx.strokeStyle = rgba(arcRgb, 0.95);\n        stroke(Math.floor(tail * n), Math.ceil(head * n));\n        const hi = Math.round(head * n);\n        if (head < 1 && visible[hi]) {\n          ctx.beginPath();\n          ctx.arc(cx + projected[hi][0] * R, cy - projected[hi][1] * R, 2, 0, Math.PI * 2);\n          ctx.fillStyle = rgba(arcRgb, 1);\n          ctx.fill();\n        }\n      });\n\n      // Markers: faint on the far side, pulsing with labels in front.\n      ctx.font = font;\n      ctx.textBaseline = \"middle\";\n      if (\"letterSpacing\" in ctx) ctx.letterSpacing = \"1.2px\";\n      points.forEach((v, index) => {\n        const p = rotate(v, cyaw, syaw, cp, sp);\n        const x = cx + p[0] * R;\n        const y = cy - p[1] * R;\n        if (p[2] <= 0) {\n          ctx.beginPath();\n          ctx.arc(x, y, 1.5, 0, Math.PI * 2);\n          ctx.fillStyle = rgba(accent, 0.22);\n          ctx.fill();\n          return;\n        }\n        const fade = Math.min(1, p[2] / 0.25);\n        if (animated) {\n          const ph = ((clock + index * 530) % PULSE_MS) / PULSE_MS;\n          ctx.beginPath();\n          ctx.arc(x, y, 3 + ph * 13, 0, Math.PI * 2);\n          ctx.lineWidth = 1;\n          ctx.strokeStyle = rgba(accent, (1 - ph) * 0.65 * fade);\n          ctx.stroke();\n        }\n        ctx.beginPath();\n        ctx.arc(x, y, 3, 0, Math.PI * 2);\n        ctx.fillStyle = rgba(accent, 0.35 + 0.65 * fade);\n        ctx.fill();\n        const name = markers[index]?.label;\n        if (showLabels && name && p[2] > 0.15) {\n          ctx.fillStyle = rgba(ink, 0.75 * Math.min(1, (p[2] - 0.15) / 0.2));\n          ctx.fillText(name.toUpperCase(), x + 9, y);\n        }\n      });\n\n      // Keyboard focus: a dashed accent ring around the silhouette.\n      if (focusVisible) {\n        ctx.beginPath();\n        ctx.setLineDash([3, 4]);\n        ctx.arc(cx, cy, R + 8, 0, Math.PI * 2);\n        ctx.lineWidth = 1.5;\n        ctx.strokeStyle = rgba(accent, 0.9);\n        ctx.stroke();\n        ctx.setLineDash([]);\n      }\n    };\n\n    const clampPitch = () => {\n      view.pitch = Math.max(-1.2, Math.min(1.2, view.pitch));\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      const dt = Math.min(64, last ? now - last : 16.7);\n      last = now;\n      clock += dt;\n\n      if (!drag.active) {\n        // Inertia decays frame-rate independently; auto-rotation continues underneath.\n        const decay = Math.pow(0.94, dt / 16.7);\n        view.yaw += view.yawVel * dt;\n        view.pitch += view.pitchVel * dt;\n        view.yawVel *= decay;\n        view.pitchVel *= decay;\n        if (Math.abs(view.yawVel) < 1e-6) view.yawVel = 0;\n        if (Math.abs(view.pitchVel) < 1e-6) view.pitchVel = 0;\n        if (autoRotate) view.yaw += dt * 0.00016 * speed;\n        clampPitch();\n      }\n      yawRef.current = view.yaw;\n      draw();\n      schedule();\n    };\n\n    const schedule = () => {\n      if (!raf && inView && !reduceMotion && !document.hidden) raf = requestAnimationFrame(frame);\n    };\n    const stop = () => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n      last = 0;\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      draw();\n      schedule();\n    };\n\n    const onThemeChange = () => {\n      readTheme();\n      draw();\n    };\n\n    /* ------------------------------- Input -------------------------------- */\n\n    const radiusPx = () => Math.max(40, Math.min(width, height) * radius);\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.pointerType === \"mouse\" && e.button !== 0) return;\n      drag.active = true;\n      drag.id = e.pointerId;\n      drag.x = e.clientX;\n      drag.y = e.clientY;\n      drag.t = e.timeStamp;\n      view.yawVel = 0;\n      view.pitchVel = 0;\n      root.setPointerCapture(e.pointerId);\n      root.style.cursor = \"grabbing\";\n    };\n    const onPointerMove = (e: PointerEvent) => {\n      if (!drag.active || e.pointerId !== drag.id) return;\n      const r = radiusPx();\n      const dx = (e.clientX - drag.x) / r;\n      // Touch keeps vertical movement for page scrolling (touch-action: pan-y), so only mouse/pen tilt.\n      const dy = e.pointerType === \"touch\" ? 0 : (e.clientY - drag.y) / r;\n      const dt = Math.max(1, e.timeStamp - drag.t);\n      view.yaw += dx;\n      view.pitch += dy;\n      clampPitch();\n      // Smoothed velocity for the release fling.\n      view.yawVel = view.yawVel * 0.5 + (dx / dt) * 0.5;\n      view.pitchVel = view.pitchVel * 0.5 + (dy / dt) * 0.5;\n      drag.x = e.clientX;\n      drag.y = e.clientY;\n      drag.t = e.timeStamp;\n      yawRef.current = view.yaw;\n      if (reduceMotion) draw();\n    };\n    const onPointerUp = (e: PointerEvent) => {\n      if (!drag.active || e.pointerId !== drag.id) return;\n      drag.active = false;\n      root.style.cursor = \"\";\n      if (root.hasPointerCapture(e.pointerId)) root.releasePointerCapture(e.pointerId);\n      // A pause before release cancels the fling; reduced motion never flings.\n      if (reduceMotion || e.timeStamp - drag.t > 80) {\n        view.yawVel = 0;\n        view.pitchVel = 0;\n      }\n    };\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      const dir =\n        e.key === \"ArrowLeft\" ? [-1, 0] : e.key === \"ArrowRight\" ? [1, 0] : e.key === \"ArrowUp\" ? [0, 1] : e.key === \"ArrowDown\" ? [0, -1] : null;\n      if (!dir) return;\n      e.preventDefault();\n      if (reduceMotion) {\n        view.yaw += dir[0] * KEY_STEP;\n        view.pitch += dir[1] * KEY_STEP;\n        clampPitch();\n        yawRef.current = view.yaw;\n        draw();\n      } else {\n        view.yawVel += dir[0] * KEY_IMPULSE;\n        view.pitchVel += dir[1] * KEY_IMPULSE;\n      }\n    };\n    const onFocus = () => {\n      focusVisible = root.matches(\":focus-visible\");\n      draw();\n    };\n    const onBlur = () => {\n      focusVisible = false;\n      draw();\n    };\n\n    const onVisibility = () => (document.hidden ? stop() : schedule());\n\n    const io = new IntersectionObserver(([entry]) => {\n      inView = entry.isIntersecting;\n      if (inView) schedule();\n      else stop();\n    });\n\n    readTheme();\n    resize();\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    io.observe(root);\n    const unobserveTheme = observeTheme(root, onThemeChange);\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    if (draggable) {\n      root.addEventListener(\"pointerdown\", onPointerDown);\n      root.addEventListener(\"pointermove\", onPointerMove);\n      root.addEventListener(\"pointerup\", onPointerUp);\n      root.addEventListener(\"pointercancel\", onPointerUp);\n      root.addEventListener(\"keydown\", onKeyDown);\n      root.addEventListener(\"focus\", onFocus);\n      root.addEventListener(\"blur\", onBlur);\n    }\n\n    return () => {\n      stop();\n      ro.disconnect();\n      io.disconnect();\n      unobserveTheme();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      root.removeEventListener(\"pointerdown\", onPointerDown);\n      root.removeEventListener(\"pointermove\", onPointerMove);\n      root.removeEventListener(\"pointerup\", onPointerUp);\n      root.removeEventListener(\"pointercancel\", onPointerUp);\n      root.removeEventListener(\"keydown\", onKeyDown);\n      root.removeEventListener(\"focus\", onFocus);\n      root.removeEventListener(\"blur\", onBlur);\n      root.style.cursor = \"\";\n    };\n  }, [markers, arcs, autoRotate, speed, gridStep, tilt, color, arcColor, showLabels, draggable, radius, reduceMotion]);\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"img\"\n      aria-label={label ?? describe(markers, arcs, draggable)}\n      tabIndex={draggable ? 0 : undefined}\n      className={`relative h-full w-full select-none outline-none ${draggable ? \"cursor-grab touch-pan-y\" : \"\"} ${className}`}\n      {...props}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 block h-full w-full text-zinc-900 dark:text-zinc-100\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "3d"
  ],
  "docs": "Documentation: https://wancraft.my/components/wireframe-globe"
}
