{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plane-hero",
  "type": "registry:block",
  "title": "Plane Hero",
  "description": "A centred headline over a dotted WebGL globe. City chips ride the sphere on CSS anchor positioning and fade as they rotate away, while great-circle arcs redraw on a stagger with a 3D plane at each peak, yawed to its own flight bearing. Plane model: chroma3d on Sketchfab (https://sketchfab.com/vendol21). Globe by cobe, vendored under MIT.",
  "github": "vij-sameerb5",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "demo.tsx",
      "content": "/**\n * Zepa UI — plane-hero\n * https://zepa.design/components/plane-hero\n * Free to use in your projects. Please don't republish or resell as your own.\n */\n\"use client\"\n\nimport { Globe, type Arc, type Marker } from \"./ui/globe\"\n\n/* ─────────────────────────────────────────────\n   plane-hero — dotted globe with flying routes\n\n   A centred headline over a WebGL globe made of\n   ~16k dots sampled off a world map. Ten cities\n   carry anchored labels; six great-circle arcs draw\n   themselves between them on a staggered loop, and a\n   3D plane rides each arc's peak, yawed to the real\n   geographic bearing of its own flight line so it\n   always points where it is going.\n\n   The labels are placed with CSS anchor positioning\n   rather than measured coordinates — cobe exposes a\n   --cobe-<id> anchor and a --cobe-visible-<id>\n   number per marker, so a chip can be pinned to a\n   point on a rotating sphere and faded as it passes\n   behind, in CSS alone.\n\n   cobe is vendored into ui/cobe.ts rather than\n   installed, so the component owns its own source\n   and needs no npm install. Its MIT notice travels\n   with it.\n\n   Plane model: chroma3d on Sketchfab\n   https://sketchfab.com/vendol21\n   Adapted from the GlobalCare.ai hero.\n   ───────────────────────────────────────────── */\n\n/**\n * Where the 3D plane is served from.\n *\n * The model itself lives at ui/plane.glb so it travels with the component in\n * source control. It cannot be loaded from there at runtime, though: only\n * public/ is served by Next, and scripts/build-registry.ts collects .tsx/.ts/\n * .json and reads them as UTF-8, so a binary in ui/ is skipped by the registry\n * and never reaches anyone installing this.\n *\n * Empty by default, which is a complete component — every arc renders its text\n * route chip and the model-viewer script never loads. Point this at a served\n * copy (public/, or any https URL) and the planes appear.\n */\nconst PLANE_MODEL = \"\"\n\nconst LOGO =\n  \"https://res.cloudinary.com/dakrfj1oh/image/upload/v1783958234/zepa22_vuauko.png\"\n\n// [lat, lng] — cobe is lat-first\nconst MARKERS: Marker[] = [\n  { id: \"sf\", location: [37.7749, -122.4194], label: \"San Francisco\" },\n  { id: \"nyc\", location: [40.7128, -74.006], label: \"New York\" },\n  { id: \"london\", location: [51.5072, -0.1276], label: \"London\" },\n  { id: \"berlin\", location: [52.52, 13.405], label: \"Berlin\" },\n  { id: \"lagos\", location: [6.5244, 3.3792], label: \"Lagos\" },\n  { id: \"bangalore\", location: [12.9716, 77.5946], label: \"Bangalore\" },\n  { id: \"singapore\", location: [1.3521, 103.8198], label: \"Singapore\" },\n  { id: \"tokyo\", location: [35.6762, 139.6503], label: \"Tokyo\" },\n  { id: \"saopaulo\", location: [-23.5505, -46.6333], label: \"São Paulo\" },\n  { id: \"sydney\", location: [-33.8688, 151.2093], label: \"Sydney\" },\n]\n\nconst ARCS: Arc[] = [\n  {\n    id: \"sf-nyc\",\n    from: [37.7749, -122.4194],\n    to: [40.7128, -74.006],\n    label: \"SF → NYC\",\n  },\n  {\n    id: \"nyc-london\",\n    from: [40.7128, -74.006],\n    to: [51.5072, -0.1276],\n    label: \"NYC → London\",\n  },\n  {\n    id: \"london-bangalore\",\n    from: [51.5072, -0.1276],\n    to: [12.9716, 77.5946],\n    label: \"London → Bangalore\",\n  },\n  {\n    id: \"berlin-lagos\",\n    from: [52.52, 13.405],\n    to: [6.5244, 3.3792],\n    label: \"Berlin → Lagos\",\n  },\n  {\n    id: \"singapore-tokyo\",\n    from: [1.3521, 103.8198],\n    to: [35.6762, 139.6503],\n    label: \"Singapore → Tokyo\",\n  },\n  {\n    id: \"saopaulo-sydney\",\n    from: [-23.5505, -46.6333],\n    to: [-33.8688, 151.2093],\n    label: \"São Paulo → Sydney\",\n  },\n]\n\nexport default function PlaneHero() {\n  return (\n    <section className=\"ph-root\">\n      <style>{CSS}</style>\n\n      {/* soft fade at the bottom */}\n      <div className=\"ph-fade\" />\n\n      <header className=\"ph-nav\">\n        <img className=\"ph-logo\" src={LOGO} alt=\"Zepa\" draggable={false} />\n        <a\n          className=\"ph-nav-link\"\n          href=\"https://zepa.design/components\"\n          target=\"_blank\"\n          rel=\"noreferrer noopener\"\n        >\n          Browse components\n        </a>\n      </header>\n\n      <div className=\"ph-copy\">\n        <p className=\"ph-eyebrow\">Open source · no build step</p>\n        <h1 className=\"ph-title\">\n          Components\n          <br />\n          without borders.\n        </h1>\n        <div className=\"ph-cta-row\">\n          <a\n            className=\"ph-cta\"\n            href=\"https://zepa.design/components\"\n            target=\"_blank\"\n            rel=\"noreferrer noopener\"\n          >\n            Start building\n            <span className=\"ph-cta-arrow\">→</span>\n          </a>\n        </div>\n      </div>\n\n      <div className=\"ph-stage\">\n        {/* light-blue glow — sized to hug the sphere (sphere ≈ 85% of the canvas) */}\n        <div className=\"ph-glow\" />\n        {/* soft horizon accent at the top edge of the sphere */}\n        <div className=\"ph-horizon\" />\n        <Globe\n          markers={MARKERS}\n          arcs={ARCS}\n          arcIcon={PLANE_MODEL || undefined}\n          markerColor={[0.15, 0.39, 0.92]}\n          baseColor={[0.91, 0.91, 0.91]}\n          arcColor={[0.15, 0.39, 0.92]}\n          glowColor={[0.82, 0.85, 0.9]}\n          dark={0}\n          mapBrightness={10}\n          markerSize={0.04}\n          markerElevation={0.01}\n        />\n      </div>\n\n      <div className=\"ph-meta ph-meta--left\">Copy the source · own it outright</div>\n      <div className=\"ph-meta ph-meta--right\">\n        Heroes · grids · animations — one registry\n      </div>\n    </section>\n  )\n}\n\nconst CSS = `\n.ph-root {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  min-height: 100vh;\n  min-height: 100svh;\n  overflow: hidden;\n  padding-bottom: 40px;\n  background-color: #f6f9ff;\n  color: #020617;\n  font-family: var(--font-manrope), ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.ph-root *, .ph-root *::before, .ph-root *::after { box-sizing: border-box; }\n\n.ph-fade {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: 128px;\n  z-index: 6;\n  pointer-events: none;\n  background: linear-gradient(to top, #ffffff, transparent);\n}\n\n.ph-nav {\n  position: relative;\n  z-index: 10;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 20px 24px;\n}\n.ph-logo {\n  height: 36px;\n  width: auto;\n  display: block;\n  user-select: none;\n  -webkit-user-select: none;\n}\n.ph-nav-link {\n  font-size: 13px;\n  font-weight: 500;\n  color: #475569;\n  text-decoration: none;\n  padding: 8px 14px;\n  border-radius: 999px;\n  border: 1px solid rgba(148, 163, 184, 0.4);\n  background: rgba(255, 255, 255, 0.7);\n  -webkit-backdrop-filter: blur(8px);\n  backdrop-filter: blur(8px);\n  transition: color 0.2s, border-color 0.2s;\n}\n.ph-nav-link:hover { color: #020617; border-color: rgba(100, 116, 139, 0.6); }\n\n.ph-copy {\n  position: relative;\n  z-index: 10;\n  margin-top: 0.5vh;\n  padding: 0 24px;\n  text-align: center;\n  pointer-events: none;\n}\n.ph-eyebrow {\n  pointer-events: auto;\n  display: inline-block;\n  margin: 0 auto 12px;\n  padding: 4px 14px;\n  border-radius: 999px;\n  border: 1px solid rgba(191, 219, 254, 0.8);\n  background: rgba(255, 255, 255, 0.7);\n  -webkit-backdrop-filter: blur(8px);\n  backdrop-filter: blur(8px);\n  box-shadow: 0 0 16px rgba(59, 130, 246, 0.35);\n  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-size: 10px;\n  text-transform: uppercase;\n  letter-spacing: 0.25em;\n  color: #64748b;\n}\n.ph-title {\n  pointer-events: auto;\n  margin: 0;\n  font-size: clamp(2.5rem, 8.5vw, 7.5rem);\n  font-weight: 600;\n  line-height: 0.95;\n  letter-spacing: -0.04em;\n  color: #020617;\n}\n.ph-cta-row { display: flex; justify-content: center; margin-top: 20px; }\n.ph-cta {\n  pointer-events: auto;\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  padding: 12px 28px;\n  border-radius: 999px;\n  background: #020617;\n  color: #ffffff;\n  font-size: 14px;\n  font-weight: 500;\n  text-decoration: none;\n  transition: background-color 0.2s;\n}\n.ph-cta:hover { background: #2563eb; }\n.ph-cta-arrow { transition: transform 0.2s; }\n.ph-cta:hover .ph-cta-arrow { transform: translateX(2px); }\n\n/* ── the globe ── */\n.ph-stage {\n  position: relative;\n  z-index: 5;\n  width: 92vw;\n  max-width: 820px;\n  margin: 24px auto 0;\n}\n.ph-glow {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  width: 100%;\n  aspect-ratio: 1;\n  transform: translate(-50%, -50%);\n  border-radius: 50%;\n  pointer-events: none;\n  filter: blur(40px);\n  background: radial-gradient(circle,\n    rgba(96, 165, 250, 0.35) 0%,\n    rgba(147, 197, 253, 0.22) 38%,\n    rgba(191, 219, 254, 0.12) 52%,\n    transparent 66%);\n}\n.ph-horizon {\n  position: absolute;\n  left: 50%;\n  top: 9%;\n  width: 70%;\n  height: 80px;\n  transform: translateX(-50%);\n  border-radius: 100%;\n  pointer-events: none;\n  filter: blur(64px);\n  background: rgba(56, 189, 248, 0.25);\n}\n\n.ph-globe {\n  position: relative;\n  aspect-ratio: 1;\n  width: 100%;\n  user-select: none;\n  -webkit-user-select: none;\n  opacity: 0;\n}\n/* entrance: starts at 200% and eases down to its normal size */\n.ph-globe.is-ready { animation: ph-globe-appear 2000ms cubic-bezier(0.16, 1, 0.3, 1) both; }\n@keyframes ph-globe-appear {\n  from { scale: 2; opacity: 0; }\n  to   { scale: 1; opacity: 1; }\n}\n.ph-canvas {\n  width: 100%;\n  height: 100%;\n  cursor: grab;\n  opacity: 0;\n  transition: opacity 1.2s ease;\n  border-radius: 50%;\n  touch-action: none;\n}\n\n/* ── anchored labels ──\n   cobe publishes a --cobe-<id> anchor name and a --cobe-visible-<id> number\n   for every marker and arc, so these chips ride the sphere and fade as their\n   point rotates out of view without a single measured coordinate. */\n.ph-pin, .ph-arc {\n  position: absolute;\n  bottom: anchor(top);\n  left: anchor(center);\n  translate: -50% 0;\n  pointer-events: none;\n  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-size: 0.6rem;\n  letter-spacing: 0.08em;\n  text-transform: uppercase;\n  white-space: nowrap;\n  transition: opacity 0.8s, filter 0.8s;\n}\n.ph-pin {\n  margin-bottom: 8px;\n  padding: 2px 6px;\n  background: #1a1a2e;\n  color: #ffffff;\n}\n.ph-pin-tip {\n  position: absolute;\n  top: 100%;\n  left: 50%;\n  transform: translate3d(-50%, -1px, 0);\n  border: 5px solid transparent;\n  border-top-color: #1a1a2e;\n}\n\n.ph-arc--text {\n  margin-bottom: 8px;\n  padding: 2px 6px;\n  background: #ffffff;\n  color: #1a1a2e;\n  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);\n  transition: opacity 0.3s, filter 0.3s;\n}\n.ph-arc-tip {\n  position: absolute;\n  top: 100%;\n  left: 50%;\n  transform: translate3d(-50%, -1px, 0);\n  border: 5px solid transparent;\n  border-top-color: #ffffff;\n}\n\n.ph-arc--model {\n  margin-bottom: -6px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 2px;\n  transition: opacity 0.3s, filter 0.3s;\n}\n.ph-route {\n  padding: 2px 6px;\n  border-radius: 3px;\n  background: #ffffff;\n  color: #1a1a2e;\n  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);\n}\n\n/* ── corner meta ── */\n.ph-meta {\n  position: absolute;\n  bottom: 24px;\n  z-index: 10;\n  display: none;\n  pointer-events: none;\n  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-size: 11px;\n  text-transform: uppercase;\n  letter-spacing: 0.2em;\n  color: #94a3b8;\n}\n.ph-meta--left { left: 32px; }\n.ph-meta--right { right: 32px; }\n\n@media (min-width: 640px) {\n  .ph-nav { padding: 20px 40px; }\n  .ph-logo { height: 40px; }\n  .ph-stage { margin-top: -56px; }\n  .ph-meta { display: block; }\n}\n\n@media (max-width: 639px) {\n  .ph-nav-link { display: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ph-globe.is-ready { animation: none; opacity: 1; }\n  .ph-canvas { transition: none; }\n  .ph-pin, .ph-arc { transition: none; }\n}\n`\n\nexport const __demoId = \"48d83beff781\"\n",
      "type": "registry:example",
      "target": "components/plane-hero/demo.tsx"
    },
    {
      "path": "ui/cobe.ts",
      "content": "/**\n * Zepa UI — plane-hero\n * https://zepa.design/components/plane-hero\n * Free to use in your projects. Please don't republish or resell as your own.\n */\n/*!\n * cobe v2.0.1 — vendored so this component needs no npm install.\n * https://github.com/shuding/cobe\n *\n * MIT License\n *\n * Copyright (c) 2021 Shu Ding\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* eslint-disable -- vendored third-party bundle, not ours to restyle */\n// @ts-nocheck -- minified bundle; not ours to type\n\nfunction Re(e,t,r){let a=e.createShader(t);return e.shaderSource(a,r),e.compileShader(a),e.getShaderParameter(a,e.COMPILE_STATUS)?a:(e.deleteShader(a),null)}function Y(e,t,r){let a=Re(e,e.VERTEX_SHADER,t),o=Re(e,e.FRAGMENT_SHADER,r);if(!a||!o)return null;let i=e.createProgram();return e.attachShader(i,a),e.attachShader(i,o),e.linkProgram(i),e.getProgramParameter(i,e.LINK_STATUS)?(e.deleteShader(a),e.deleteShader(o),i):(e.deleteProgram(i),null)}function H(e,t,r){let a={};for(let o of r)a[o]=e.getUniformLocation(t,o);return a}function Q(e,t,r){let a={};for(let o of r)a[o]=e.getAttribLocation(t,o);return a}function Ae(e){let t={},r={},a={},o=document.createElement(\"style\");function i(t,r,a,o){let i=t[r];i||(i=document.createElement(\"div\"),i.style.cssText=\"position:absolute;width:1px;height:1px;pointer-events:none;anchor-name:\"+a,e.append(i),t[r]=i),i.style.left=100*o.x+\"%\",i.style.top=100*o.y+\"%\"}return document.head.append(o),{m:function(e,r){let o={};for(let n of e){let e=n.id;if(!e)continue;let f=r(n.location);o[e]=1,i(t,e,\"--cobe-\"+e,f),f.visible?a[\"--cobe-visible-\"+e]=\"N\":delete a[\"--cobe-visible-\"+e]}for(let e in t)o[e]||(t[e].remove(),delete t[e],delete a[\"--cobe-visible-\"+e])},a:function(e,t){let o={};for(let n of e){let e=n.id;if(!e)continue;let f=t(n);o[e]=1,i(r,e,\"--cobe-arc-\"+e,f),f.visible?a[\"--cobe-visible-arc-\"+e]=\"N\":delete a[\"--cobe-visible-arc-\"+e]}for(let e in r)o[e]||(r[e].remove(),delete r[e],delete a[\"--cobe-visible-arc-\"+e])},r:function(){for(let e in t)t[e].remove();for(let e in r)r[e].remove();o.remove()},s:function(){let e=\"\";for(let t in a)e+=t+\":\"+a[t]+\";\";o.textContent=\":root{\"+e+\"}\"}}}var{PI:Z,sin:le,cos:_e}=Math,Be=\"attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}\",Ce=\"precision highp float;uniform vec2 t,v,s;uniform vec3 F,w;uniform vec4 n;uniform float k,x,y;uniform sampler2D z;float u;mat3 A(float a,float b){float c=cos(a),d=cos(b),e=sin(a),f=sin(b);return mat3(d,f*e,-f*c,0,c,e,f,d*-e,d*c);}vec3 B(vec3 c,out float G){c=c.xzy;float q=max(2.,floor(log2(2.236068*k*3.141593*(1.-c.z*c.z))*.72021));vec2 g=floor(pow(1.618034,q)/2.236068*vec2(1,1.618034)+.5),d=fract((g+1.)*.618034)*6.283185-3.883222,e=-2.*g,f=vec2(atan(c.y,c.x),c.z-1.),r=floor(vec2(e.y*f.x-d.y*(f.y*k+1.),-e.x*f.x+d.x*(f.y*k+1.))/(d.x*e.y-e.x*d.y));float o=3.141593;vec3 C;for(float h=0.;h<4.;h+=1.){vec2 D=vec2(mod(h,2.),floor(h*.5));float j=dot(g,r+D);if(j>k)continue;float a=j,b=0.;a>=16384.?(a-=16384.,b+=.868872):0.,a>=8192.?(a-=8192.,b+=.934436):0.,a>=4096.?(a-=4096.,b+=.467218):0.,a>=2048.?(a-=2048.,b+=.733609):0.,a>=1024.?(a-=1024.,b+=.866804):0.,a>=512.?(a-=512.,b+=.433402):0.,a>=256.?(a-=256.,b+=.216701):0.,a>=128.?(a-=128.,b+=.108351):0.,a>=64.?(a-=64.,b+=.554175):0.,a>=32.?(a-=32.,b+=.777088):0.,a>=16.?(a-=16.,b+=.888544):0.,a>=8.?(a-=8.,b+=.944272):0.,a>=4.?(a-=4.,b+=.472136):0.,a>=2.?(a-=2.,b+=.236068):0.,a>=1.?(a-=1.,b+=.618034):0.;float l=fract(b)*6.283185,i=1.-2.*j*u,m=sqrt(1.-i*i);vec3 p=vec3(cos(l)*m,sin(l)*m,i);float E=length(c-p);if(E<o)o=E,C=p;}G=o;return C.xzy;}void main(){u=1./k;vec2 c=1./t,b=(gl_FragCoord.xy*c*2.-1.)/x-v*vec2(1,-1)*c;b.x*=t.x*c.y;float a=dot(b,b),f=0.;vec4 l=vec4(0);if(a<=.64){float g;vec4 m=vec4(0);vec3 h=normalize(vec3(b,sqrt(.64-a)));mat3 o=A(s.y,s.x);float i=h.z;vec3 d=B(h*o,g);float j=asin(d.y),e=acos(-d.x/cos(j));e=d.z<0.?-e:e;float p=max(texture2D(z,vec2(e*.5/3.141593,-(j/3.141593+.5))).x,y),q=p*smoothstep(8e-3,0.,g)*pow(i,n.y)*n.x;m+=vec4(F*(mix((1.-q)*pow(i,.4),q,n.z)+.1)+pow(1.-i,4.)*w,1),l+=m*(1.+n.w)*.5,f=(1.-a)*(1.-a)*smoothstep(0.,1.,.2/(a-.64));}else{float r=sqrt(.2/(a-.64));f=smoothstep(.5,1.,r/(r+1.));}gl_FragColor=l+vec4(f*w,f);}\",be=\"varying vec2 m;varying vec3 g;varying float h;attribute vec2 n;attribute vec3 p,w;attribute float q,x;uniform vec2 b,r;uniform float i,j,k,s;void main(){float c=cos(j),d=sin(j),e=cos(i),f=sin(i);vec3 a=p*(.8+s),l=vec3(e*a.x+f*a.z,f*d*a.x+c*a.y-e*d*a.z,-f*c*a.x+d*a.y+e*c*a.z);if(l.z<0.&&length(l.xy)<.8){gl_Position=vec4(2,2,0,1);return;}float t=b.y/b.x;vec2 y=(l.xy+n*q*2.)*vec2(t,1)*k+r*vec2(1,-1)*k/b;gl_Position=vec4(y,0,1),m=n,g=w,h=x;}\",ge=\"precision highp float;varying vec2 m;varying vec3 g;varying float h;uniform vec3 v;void main(){if(length(m)>.25)discard;vec3 a=h>.5?g:v;gl_FragColor=vec4(a,1);}\",Fe=\"varying vec3 i;varying float j,s,t;attribute vec2 k;attribute vec3 l,m,N;attribute float v,w,O;uniform vec2 g,x;uniform float y,z,h,A;mat3 B(float a,float b){float c=cos(a),d=cos(b),e=sin(a),f=sin(b);return mat3(d,f*e,-f*c,0,c,e,f,d*-e,d*c);}vec3 C(vec3 c,vec3 d,vec3 e,float a){float b=1.-a;return b*b*c+2.*b*a*d+a*a*e;}vec3 D(vec3 c,vec3 b,vec3 d,float a){float e=1.-a;return 2.*e*(b-c)+2.*a*(d-b);}void main(){mat3 b=B(z,y);float c=.8+A;vec3 d=l*c,e=m*c,f=l+m;float n=length(f);vec3 E=n>1e-3?f/n:vec3(0,1,0),o=E*(.8+v);float p=k.x;vec3 F=C(d,o,e,p),q=b*F,G=D(d,o,e,p),H=b*G;vec2 a=H.xy;float r=length(a);vec2 I=r>1e-3?vec2(-a.y,a.x)/r:vec2(1,0);float J=g.x/g.y;vec2 K=q.xy*vec2(1./J,1)*h+x*vec2(1,-1)*h/g,P=K+I*w*k.y*h;gl_Position=vec4(P,0,1),i=N,j=O,s=q.z,t=length(q.xy);}\",Le=\"precision highp float;varying vec3 i;varying float j,s,t;uniform vec3 M;void main(){if(s<0.&&t<.8)discard;vec3 a=j>.5?i:M;gl_FragColor=vec4(a,1);}\",ee=.8;function U([e,t]){let r=e*Z/180,a=t*Z/180-Z,o=_e(r);return[-o*_e(a),le(r),o*le(a)]}var Pe=(e,t)=>{let r={alpha:!0,stencil:!1,antialias:!0,depth:!1,preserveDrawingBuffer:!1,...t.context},a=e.getContext(\"webgl2\",r),o=!!a;if(a||(a=e.getContext(\"webgl\",r)),!a)return{destroy:()=>{},update:()=>{}};let i=o?null:a.getExtension(\"ANGLE_instanced_arrays\"),n=t.devicePixelRatio||1;e.width=t.width*n,e.height=t.height*n;let f=t.phi||0,l=t.theta||0,c=t.markers||[],s=t.arcs||[],A=t.mapSamples||1e4,v=t.mapBrightness||1,d=t.mapBaseBrightness||0,g=t.baseColor||[1,1,1],m=t.markerColor||[1,.5,0],u=t.glowColor||[1,1,1],h=t.arcColor||[.3,.6,1],E=t.arcWidth??1,R=t.arcHeight??.2,b=t.diffuse||1,x=t.dark||0,y=t.opacity??1,T=t.offset||[0,0],B=t.scale||1,p=t.markerElevation??.05,w=Y(a,Be,Ce),C=Y(a,be,ge),F=Y(a,Fe,Le);if(!w)return{destroy:()=>{},update:()=>{}};let D=a.createBuffer();a.bindBuffer(a.ARRAY_BUFFER,D),a.bufferData(a.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),a.STATIC_DRAW);let _=a.createBuffer();a.bindBuffer(a.ARRAY_BUFFER,_);let N=[];for(let e=0;e<=32;e++){let t=e/32;N.push(t,-1,t,1)}a.bufferData(a.ARRAY_BUFFER,new Float32Array(N),a.STATIC_DRAW);let k=a.createBuffer(),S=a.createBuffer(),I=H(a,w,[\"t\",\"s\",\"k\",\"x\",\"v\",\"F\",\"w\",\"n\",\"y\",\"z\"]),P=H(a,C,[\"i\",\"j\",\"b\",\"k\",\"r\",\"v\",\"s\"]),L=Q(a,C,[\"n\",\"p\",\"q\",\"w\",\"x\"]),z=H(a,F,[\"y\",\"z\",\"g\",\"h\",\"x\",\"M\",\"A\"]),G=Q(a,F,[\"k\",\"l\",\"m\",\"v\",\"w\",\"N\",\"O\"]),M=a.getAttribLocation(w,\"a\"),V=a.createTexture();a.bindTexture(a.TEXTURE_2D,V),a.texImage2D(a.TEXTURE_2D,0,a.RGB,1,1,0,a.RGB,a.UNSIGNED_BYTE,new Uint8Array([0,0,0])),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.NEAREST),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.NEAREST);let j=new Image;j.onload=()=>{a.bindTexture(a.TEXTURE_2D,V),a.texImage2D(a.TEXTURE_2D,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,j),a.generateMipmap(a.TEXTURE_2D),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.NEAREST),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.NEAREST),a.activeTexture(a.TEXTURE0),a.bindTexture(a.TEXTURE_2D,V)},j.src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAACAAQAAAADMzoqnAAAECklEQVR42u3VsW4jRRzH8d94gzfF4Q0VQaC4vBLTRTp0mze4ggfAPAE5XQEFsGNAVIjwBrmW7h7gJE+giKjyABTZE4g06LKJETdRJvtD65kdz6yduKABiW+TVfzRf2bXYxtcE/59YJCz6YdbgQF6ACSRrwYKYImmh5PbwOewlV3wlQNbAN6SEExjUOO+BU0aCSnxReHABUlK4YFQeJeUT3da8IIkZ6NGoSnFY5KsMoVzMKfECUnqxgPYRArarmUCndHwzIEaQEpg5xVdBXROl8mpAQx5dUgPiHoYAAkg5w3JABR06byGAVgcRGAz5bznj6phBQNRFwyqgdxebH6gshJAesWoFhgYpApAFoG8BIZ/fEhSox5jDjQXmV0Ar5XJfAIrALi3URVs09gHIL4XJCkLC5LH9JWiArABFCSrQjdgkBzRJ0WJeUOSNyQAfJJwUSWUBRlJQ8oGHATACGlBynnzy2kEYLNjrxouigD8BZcgOeVPqh12RtufaCN5wCPVDpvQ9lsIrqndsJtDcWqBCpf4hWN7OdWHBw58FwIaNOU/n1TpMW2DFaD48cmr4185T8NHkpUFX749pQPVdgRKC/DGoQPVeAEKv+WHvY8OOWNTPRp5kHuwSf8wzXtVBKR7YwEH9H3lQUaypUfSATOALyVNu5vZJW31Bnx98nkLfDUWJaz6ixvm+RIQRdl3kmRxxiaDoGnZW4CpPfkaQadlcPim1xOSvETQo7Lv75enVAXJ3xGUlony4KQBBWUM1NiDc6qhyS8RgQs18OCMMtPDaAUIyg0PZkRWDqs+wnKJBTDI1Js6BolegOsKmUxNDBAAKqQyMQmidhegBlLZ+wwKYdv5M/8x1khkb1cgKqP2H+MKyV5vS+whrE8DQDgAlUAoRBX056EElJCjJVACeJBZgNfVp+iCCm4RBWCgKsRxASSA9KgDhDtCiTuMyfHsKXzhC6wNAIjjWb8LKAOA2ctk3FmCOlgKFy8f1N0JJtgsxinYnVAHt4t3gPzZXSCTyCWCQmBT91QE3B5yarSN40dNHYPka4TlDhTUI8zLvl0JSL3vZn6DsCFZOeB2yROEpR68sECQQA++xIGCR2X7DwlEoLRgUrZrqlUg50S1uy43YqDcN6UFBVkhAjWiCV2Q0jgQPdplMKxvBXodcOfAwJYvgdL+1etA1YJJfBcZlQV7sO1i2gHoNiyxtQ5sBsCgWyoxCHiFFd2L5nUTCqMAqGUgsQ9f5kCcCiZgRYkMgMTd5WsB1rTzj0Em14BE4r+QxN1lCEsVur2PoF5Wbg8RJXR4djgvBgauhLywoEZQrt1KKRdVS4CdlJ8qafyP+9KIj/nE/d7kKwH9jgS72e9DV+kvfTWgct4ZyP8Byb8BPG7MaaIIkAQAAAAASUVORK5CYII=\";let J=0;function O(t){let r=Math.cos(l),a=Math.cos(f),o=Math.sin(l),i=Math.sin(f),c=a*t[0]+i*t[2],s=i*o*t[0]+r*t[1]-a*o*t[2];return[(c/(e.width/e.height)*B+T[0]*B*n/e.width+1)/2,(-s*B+T[1]*B*n/e.height+1)/2,-i*r*t[0]+o*t[1]+a*r*t[2]>=0||c*c+s*s>=.64]}function W(e){let t=U(e),r=ee+p,a=O([t[0]*r,t[1]*r,t[2]*r]);return{x:a[0],y:a[1],visible:a[2]}}function X(e){let t=U(e.from),r=U(e.to),a=[t[0]+r[0],t[1]+r[1],t[2]+r[2]],o=(a[0]**2+a[1]**2+a[2]**2)**.5;if(o<.001)return null;let i=.25*(ee+p)+.5*(ee+R+p)/o,n=O([a[0]*i,a[1]*i,a[2]*i]);return{x:n[0],y:n[1],visible:n[2]}}function K(e,t,r,n,f){e<0||(a.enableVertexAttribArray(e),a.vertexAttribPointer(e,t,a.FLOAT,!1,r,n),o?a.vertexAttribDivisor(e,f):i&&i.vertexAttribDivisorANGLE(e,f))}let q,Z=document.createElement(\"div\");Z.style.cssText=\"position:relative;width:100%;height:100%\",e.parentElement?.insertBefore(Z,e),Z.append(e);let $=Ae(Z);function te(t){if(t.phi!=q&&(f=t.phi),t.theta!=q&&(l=t.theta),t.markers&&function(e){c=e;let t=new Float32Array(8*c.length);c.forEach((e,r)=>{t.set([...U(e.location),e.size,...e.color||[0,0,0],e.color?1:0],8*r)}),a.bindBuffer(a.ARRAY_BUFFER,k),a.bufferData(a.ARRAY_BUFFER,t,a.DYNAMIC_DRAW)}(t.markers),t.arcs&&function(e){s=e,J=s.length;let t=new Float32Array(12*s.length);s.forEach((e,r)=>{t.set([...U(e.from),...U(e.to),R+p,.005*E,...e.color||[0,0,0],e.color?1:0],12*r)}),a.bindBuffer(a.ARRAY_BUFFER,S),a.bufferData(a.ARRAY_BUFFER,t,a.DYNAMIC_DRAW)}(t.arcs),t.width&&t.height&&(e.width=t.width*n,e.height=t.height*n),t.mapSamples!=q&&(A=t.mapSamples),t.mapBrightness!=q&&(v=t.mapBrightness),t.mapBaseBrightness!=q&&(d=t.mapBaseBrightness),t.baseColor!=q&&(g=t.baseColor),t.markerColor!=q&&(m=t.markerColor),t.glowColor!=q&&(u=t.glowColor),t.arcColor!=q&&(h=t.arcColor),t.arcWidth!=q&&(E=t.arcWidth),t.arcHeight!=q&&(R=t.arcHeight),t.diffuse!=q&&(b=t.diffuse),t.dark!=q&&(x=t.dark),t.opacity!=q&&(y=t.opacity),t.offset!=q&&(T=t.offset),t.scale!=q&&(B=t.scale),t.markerElevation!=q&&(p=t.markerElevation),$.m(c,W),$.a(s,X),$.s(),a.viewport(0,0,e.width,e.height),a.clearColor(0,0,0,0),a.clear(a.COLOR_BUFFER_BIT),a.enable(a.BLEND),a.blendFunc(a.SRC_ALPHA,a.ONE_MINUS_SRC_ALPHA),a.useProgram(w),a.bindBuffer(a.ARRAY_BUFFER,D),a.enableVertexAttribArray(M),a.vertexAttribPointer(M,2,a.FLOAT,!1,0,0),o?a.vertexAttribDivisor(M,0):i&&i.vertexAttribDivisorANGLE(M,0),a.uniform2f(I.t,e.width,e.height),a.uniform2f(I.s,f,l),a.uniform1f(I.k,A),a.uniform1f(I.x,B),a.uniform2f(I.v,T[0]*n,T[1]*n),a.uniform3fv(I.F,g),a.uniform3fv(I.w,u),a.uniform4f(I.n,v,b,x,y),a.uniform1f(I.y,d),a.uniform1i(I.z,0),a.activeTexture(a.TEXTURE0),a.bindTexture(a.TEXTURE_2D,V),a.drawArrays(a.TRIANGLES,0,6),F&&J>0){a.useProgram(F),a.bindBuffer(a.ARRAY_BUFFER,_),G.k>=0&&(a.enableVertexAttribArray(G.k),a.vertexAttribPointer(G.k,2,a.FLOAT,!1,0,0),o?a.vertexAttribDivisor(G.k,0):i&&i.vertexAttribDivisorANGLE(G.k,0)),a.bindBuffer(a.ARRAY_BUFFER,S);let t=48;if(K(G.l,3,t,0,1),K(G.m,3,t,12,1),K(G.v,1,t,24,1),K(G.w,1,t,28,1),K(G.N,3,t,32,1),K(G.O,1,t,44,1),a.uniform1f(z.y,f),a.uniform1f(z.z,l),a.uniform2f(z.g,e.width,e.height),a.uniform1f(z.h,B),a.uniform2f(z.x,T[0]*n,T[1]*n),a.uniform3fv(z.M,h),a.uniform1f(z.A,p),o)a.drawArraysInstanced(a.TRIANGLE_STRIP,0,66,J);else if(i)i.drawArraysInstancedANGLE(a.TRIANGLE_STRIP,0,66,J);else for(let e=0;e<J;e++)a.drawArrays(a.TRIANGLE_STRIP,0,66)}if(C&&c.length>0){a.useProgram(C),a.bindBuffer(a.ARRAY_BUFFER,D),L.n>=0&&(a.enableVertexAttribArray(L.n),a.vertexAttribPointer(L.n,2,a.FLOAT,!1,0,0),o?a.vertexAttribDivisor(L.n,0):i&&i.vertexAttribDivisorANGLE(L.n,0)),a.bindBuffer(a.ARRAY_BUFFER,k);let t=32;K(L.p,3,t,0,1),K(L.q,1,t,12,1),K(L.w,3,t,16,1),K(L.x,1,t,28,1),a.uniform1f(P.i,f),a.uniform1f(P.j,l),a.uniform2f(P.b,e.width,e.height),a.uniform1f(P.k,B),a.uniform2f(P.r,T[0]*n,T[1]*n),a.uniform3fv(P.v,m),a.uniform1f(P.s,p),function(e){if(o)a.drawArraysInstanced(a.TRIANGLES,0,6,e);else if(i)i.drawArraysInstancedANGLE(a.TRIANGLES,0,6,e);else for(let t=0;t<e;t++)a.drawArrays(a.TRIANGLES,0,6)}(c.length)}}return te({markers:c,arcs:s}),{update:te,destroy:()=>{a.deleteBuffer(D),a.deleteBuffer(_),a.deleteBuffer(k),a.deleteBuffer(S),a.deleteProgram(w),C&&a.deleteProgram(C),F&&a.deleteProgram(F),$.r()}}};export{Pe as default};",
      "type": "registry:component",
      "target": "components/plane-hero/ui/cobe.ts"
    },
    {
      "path": "ui/globe.tsx",
      "content": "/**\n * Zepa UI — plane-hero\n * https://zepa.design/components/plane-hero\n * Free to use in your projects. Please don't republish or resell as your own.\n */\n\"use client\"\n\nimport { useEffect, useRef, useCallback, useState } from \"react\"\n// vendored beside this file — see ui/cobe.ts for the MIT notice\nimport createGlobe from \"./cobe\"\n\n/* arc re-fly loop: each arc is removed briefly and re-added (staggered),\n   which re-triggers cobe's own smooth draw-in animation — everywhere,\n   including the front of the globe. endpoints are never touched. */\nconst ARC_CYCLE_MS = 7000 // full loop per arc\nconst ARC_GAP_MS = 650 // how long an arc is gone before it flies again\n\n/* geographic bearing (deg from north) at the arc midpoint, heading toward `to` —\n   used to point the 3D plane along its flight line */\nconst RAD = Math.PI / 180\nfunction bearingDeg(from: [number, number], to: [number, number]): number {\n  const [lat1, lng1] = [from[0] * RAD, from[1] * RAD]\n  const [lat2, lng2] = [to[0] * RAD, to[1] * RAD]\n  const dLng = lng2 - lng1\n  const y = Math.sin(dLng) * Math.cos(lat2)\n  const x =\n    Math.cos(lat1) * Math.sin(lat2) -\n    Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng)\n  return (Math.atan2(y, x) / RAD + 360) % 360\n}\n// tweak if the plane points sideways/backwards (degrees, added to heading)\nconst ARC_ICON_YAW_OFFSET = 0\n\nexport interface Marker {\n  id: string\n  location: [number, number]\n  label: string\n}\nexport interface Arc {\n  id: string\n  from: [number, number]\n  to: [number, number]\n  label?: string\n}\ninterface GlobeProps {\n  markers?: Marker[]\n  arcs?: Arc[]\n  arcIcon?: string // path to a .glb — replaces arc text labels with the 3D model\n  className?: string\n  markerColor?: [number, number, number]\n  baseColor?: [number, number, number]\n  arcColor?: [number, number, number]\n  glowColor?: [number, number, number]\n  dark?: number\n  mapBrightness?: number\n  markerSize?: number\n  markerElevation?: number\n  arcWidth?: number\n  arcHeight?: number\n  speed?: number\n  theta?: number\n  diffuse?: number\n  mapSamples?: number\n}\n\nexport function Globe({\n  markers = [],\n  arcs = [],\n  arcIcon,\n  className = \"\",\n  markerColor = [0.3, 0.45, 0.85],\n  baseColor = [1, 1, 1],\n  arcColor = [0.3, 0.45, 0.85],\n  glowColor = [0.94, 0.93, 0.91],\n  dark = 0,\n  mapBrightness = 10,\n  markerSize = 0.025,\n  markerElevation = 0.01,\n  arcWidth = 0.5,\n  arcHeight = 0.25,\n  speed = 0.003,\n  theta = 0.2,\n  diffuse = 1.5,\n  mapSamples = 16000,\n}: GlobeProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const [ready, setReady] = useState(false) // globe drawn → start entrance animation\n  const pointerInteracting = useRef<{ x: number; y: number } | null>(null)\n  const lastPointer = useRef<{ x: number; y: number; t: number } | null>(null)\n  const dragOffset = useRef({ phi: 0, theta: 0 })\n  const velocity = useRef({ phi: 0, theta: 0 })\n  const phiOffsetRef = useRef(0)\n  const thetaOffsetRef = useRef(0)\n  const isPausedRef = useRef(false)\n\n  const handlePointerDown = useCallback((e: React.PointerEvent) => {\n    pointerInteracting.current = { x: e.clientX, y: e.clientY }\n    if (canvasRef.current) canvasRef.current.style.cursor = \"grabbing\"\n    isPausedRef.current = true\n  }, [])\n\n  const handlePointerMove = useCallback((e: PointerEvent) => {\n    if (pointerInteracting.current !== null) {\n      const deltaX = e.clientX - pointerInteracting.current.x\n      const deltaY = e.clientY - pointerInteracting.current.y\n      dragOffset.current = { phi: deltaX / 300, theta: deltaY / 1000 }\n      const now = Date.now()\n      if (lastPointer.current) {\n        const dt = Math.max(now - lastPointer.current.t, 1)\n        const maxVelocity = 0.15\n        velocity.current = {\n          phi: Math.max(\n            -maxVelocity,\n            Math.min(maxVelocity, ((e.clientX - lastPointer.current.x) / dt) * 0.3),\n          ),\n          theta: Math.max(\n            -maxVelocity,\n            Math.min(maxVelocity, ((e.clientY - lastPointer.current.y) / dt) * 0.08),\n          ),\n        }\n      }\n      lastPointer.current = { x: e.clientX, y: e.clientY, t: now }\n    }\n  }, [])\n\n  const handlePointerUp = useCallback(() => {\n    if (pointerInteracting.current !== null) {\n      phiOffsetRef.current += dragOffset.current.phi\n      thetaOffsetRef.current += dragOffset.current.theta\n      dragOffset.current = { phi: 0, theta: 0 }\n      lastPointer.current = null\n    }\n    pointerInteracting.current = null\n    if (canvasRef.current) canvasRef.current.style.cursor = \"grab\"\n    isPausedRef.current = false\n  }, [])\n\n  // load <model-viewer> once if arc icons use a .glb\n  useEffect(() => {\n    if (!arcIcon) return\n    const src =\n      \"https://cdn.jsdelivr.net/npm/@google/model-viewer@3.5.0/dist/model-viewer.min.js\"\n    if (document.querySelector(`script[src=\"${src}\"]`)) return\n    const s = document.createElement(\"script\")\n    s.type = \"module\"\n    s.src = src\n    document.head.appendChild(s)\n  }, [arcIcon])\n\n  useEffect(() => {\n    window.addEventListener(\"pointermove\", handlePointerMove, { passive: true })\n    window.addEventListener(\"pointerup\", handlePointerUp, { passive: true })\n    return () => {\n      window.removeEventListener(\"pointermove\", handlePointerMove)\n      window.removeEventListener(\"pointerup\", handlePointerUp)\n    }\n  }, [handlePointerMove, handlePointerUp])\n\n  useEffect(() => {\n    if (!canvasRef.current) return\n    const canvas = canvasRef.current\n    let globe: ReturnType<typeof createGlobe> | null = null\n    let animationId: number\n    let phi = 0\n    let lastArcKey = \"__init__\"\n\n    function init() {\n      const width = canvas.offsetWidth\n      if (width === 0 || globe) return\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      globe = createGlobe(canvas, {\n        devicePixelRatio: dpr,\n        width,\n        height: width,\n        phi: 0,\n        theta,\n        dark,\n        diffuse,\n        mapSamples,\n        mapBrightness,\n        baseColor,\n        markerColor,\n        glowColor,\n        markerElevation,\n        markers: markers.map((m) => ({\n          location: m.location,\n          size: markerSize,\n          id: m.id,\n        })),\n        arcs: arcs.map((a) => ({ from: a.from, to: a.to, id: a.id })),\n        arcColor,\n        arcWidth,\n        arcHeight,\n        opacity: 0.7,\n      })\n\n      function animate() {\n        if (!isPausedRef.current) {\n          phi += speed\n          if (\n            Math.abs(velocity.current.phi) > 0.0001 ||\n            Math.abs(velocity.current.theta) > 0.0001\n          ) {\n            phiOffsetRef.current += velocity.current.phi\n            thetaOffsetRef.current += velocity.current.theta\n            velocity.current.phi *= 0.95\n            velocity.current.theta *= 0.95\n          }\n          const thetaMin = -0.4\n          const thetaMax = 0.4\n          if (thetaOffsetRef.current < thetaMin) {\n            thetaOffsetRef.current += (thetaMin - thetaOffsetRef.current) * 0.1\n          } else if (thetaOffsetRef.current > thetaMax) {\n            thetaOffsetRef.current += (thetaMax - thetaOffsetRef.current) * 0.1\n          }\n        }\n        // staggered re-fly: drop each arc briefly, then re-add it so cobe\n        // replays its native draw-in animation. arcs only sent on change.\n        const nowMs = performance.now()\n        const presentArcs = arcs.filter(\n          (_, i) =>\n            (nowMs + (i * ARC_CYCLE_MS) / Math.max(arcs.length, 1)) % ARC_CYCLE_MS >\n            ARC_GAP_MS,\n        )\n        const arcKey = presentArcs.map((a) => a.id).join(\",\")\n        const payload: Parameters<NonNullable<typeof globe>[\"update\"]>[0] = {\n          phi: phi + phiOffsetRef.current + dragOffset.current.phi,\n          theta: theta + thetaOffsetRef.current + dragOffset.current.theta,\n        }\n        if (arcKey !== lastArcKey) {\n          lastArcKey = arcKey\n          payload.arcs = presentArcs.map((a) => ({\n            from: a.from,\n            to: a.to,\n            id: a.id,\n          }))\n        }\n        globe!.update(payload)\n        animationId = requestAnimationFrame(animate)\n      }\n      animate()\n      setTimeout(() => {\n        if (canvas) canvas.style.opacity = \"1\"\n        setReady(true)\n      })\n    }\n\n    if (canvas.offsetWidth > 0) {\n      init()\n    } else {\n      const ro = new ResizeObserver((entries) => {\n        if (entries[0]?.contentRect.width > 0) {\n          ro.disconnect()\n          init()\n        }\n      })\n      ro.observe(canvas)\n    }\n    return () => {\n      if (animationId) cancelAnimationFrame(animationId)\n      if (globe) globe.destroy()\n    }\n  }, [\n    markers,\n    arcs,\n    markerColor,\n    baseColor,\n    arcColor,\n    glowColor,\n    dark,\n    mapBrightness,\n    markerSize,\n    markerElevation,\n    arcWidth,\n    arcHeight,\n    speed,\n    theta,\n    diffuse,\n    mapSamples,\n  ])\n\n  return (\n    <div className={`ph-globe ${ready ? \"is-ready\" : \"\"} ${className}`}>\n      <canvas\n        ref={canvasRef}\n        className=\"ph-canvas\"\n        onPointerDown={handlePointerDown}\n      />\n\n      {markers.map((m) => (\n        <div\n          key={m.id}\n          className=\"ph-pin\"\n          style={\n            {\n              positionAnchor: `--cobe-${m.id}`,\n              opacity: `var(--cobe-visible-${m.id}, 0)`,\n              filter: `blur(calc((1 - var(--cobe-visible-${m.id}, 0)) * 8px))`,\n            } as React.CSSProperties\n          }\n        >\n          {m.label}\n          <span className=\"ph-pin-tip\" />\n        </div>\n      ))}\n\n      {arcs\n        .filter((a) => a.label || arcIcon)\n        .map((a) =>\n          arcIcon ? (\n            // 3D plane + route label, stacked at the arc's peak\n            <div\n              key={a.id}\n              className=\"ph-arc ph-arc--model\"\n              style={\n                {\n                  positionAnchor: `--cobe-arc-${a.id}`,\n                  opacity: `var(--cobe-visible-arc-${a.id}, 0)`,\n                  filter: `blur(calc((1 - var(--cobe-visible-arc-${a.id}, 0)) * 8px))`,\n                } as React.CSSProperties\n              }\n            >\n              {/* @ts-expect-error web component */}\n              <model-viewer\n                src={arcIcon}\n                orientation={`0deg 0deg ${-(bearingDeg(a.from, a.to) + ARC_ICON_YAW_OFFSET)}deg`}\n                camera-orbit=\"0deg 72deg 105%\"\n                interaction-prompt=\"none\"\n                disable-zoom\n                disable-tap\n                shadow-intensity=\"0\"\n                style={{ width: 64, height: 64, background: \"transparent\" }}\n              />\n              {a.label && <span className=\"ph-route\">{a.label}</span>}\n            </div>\n          ) : (\n            <div\n              key={a.id}\n              className=\"ph-arc ph-arc--text\"\n              style={\n                {\n                  positionAnchor: `--cobe-arc-${a.id}`,\n                  opacity: `var(--cobe-visible-arc-${a.id}, 0)`,\n                  filter: `blur(calc((1 - var(--cobe-visible-arc-${a.id}, 0)) * 8px))`,\n                } as React.CSSProperties\n              }\n            >\n              {a.label}\n              <span className=\"ph-arc-tip\" />\n            </div>\n          ),\n        )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/plane-hero/ui/globe.tsx"
    }
  ]
}
