shadimage
flightcn
blockaircraft-range-explorersource: ok · hit

Aircraft Range Explorer

Compare aircraft reach from a departure airport, with labeled 75% and maximum-range boundaries and a live destination count.

npx shadcn add "https://flightcn.yencheng.dev/r/aircraft-range-explorer.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

@flightcn/flighthttps://mapcn.dev/r/map.json

Files1 · source cached 10/14/2026, 12:02:26 PM

src/registry/blocks/aircraft-range-explorer/aircraft-range-explorer.tsx
"use client";

import { Compass } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Map, useMap } from "@/components/ui/map";
import {
  FlightRange,
  getAirportInfo,
  SingleWorldZoomLimit,
} from "@/components/ui/flight";

const HUBS = ["TPE", "DXB", "LHR", "JFK"] as const;

type Aircraft = {
  model: string;
  /** Maximum range in kilometres. */
  rangeKm: number;
  seats: number;
};

const AIRCRAFT: readonly Aircraft[] = [
  { model: "A321XLR", rangeKm: 8700, seats: 180 },
  { model: "787-9", rangeKm: 14140, seats: 290 },
  { model: "777-300ER", rangeKm: 13650, seats: 365 },
  { model: "A350-900", rangeKm: 15372, seats: 325 },
];

/** Major destinations used for the reachability readout. */
const DESTINATIONS = [
  "HND",
  "ICN",
  "SIN",
  "SYD",
  "AKL",
  "DEL",
  "DXB",
  "IST",
  "CAI",
  "NBO",
  "JNB",
  "LHR",
  "CDG",
  "FRA",
  "AMS",
  "MAD",
  "JFK",
  "ORD",
  "ATL",
  "LAX",
  "SFO",
  "YYZ",
  "MEX",
  "GRU",
  "SCL",
] as const;

const EARTH_RADIUS_KM = 6371;
const REFERENCE_COLOR = "#0284c7";
const MAX_RANGE_COLOR = "#c65d24";
const formatKm = (distance: number) => `${distance.toLocaleString("en-US")} km`;

function greatCircleKm(from: string, to: string) {
  const a = getAirportInfo(from);
  const b = getAirportInfo(to);
  if (!a || !b) return Infinity;
  const toRadians = Math.PI / 180;
  const dLat = (b.latitude - a.latitude) * toRadians;
  const dLng = (b.longitude - a.longitude) * toRadians;
  const h =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(a.latitude * toRadians) *
      Math.cos(b.latitude * toRadians) *
      Math.sin(dLng / 2) ** 2;
  return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(h)));
}

function HubCamera({ hub }: { hub: string }) {
  const { map, isLoaded } = useMap();
  useEffect(() => {
    if (!map || !isLoaded) return;
    const info = getAirportInfo(hub);
    if (!info) return;
    map.flyTo({
      center: [info.longitude, info.latitude],
      zoom: 1.4,
      duration: 1200,
    });
  }, [map, isLoaded, hub]);
  return null;
}

export function AircraftRangeExplorer() {
  const [hub, setHub] = useState<string>(HUBS[0]);
  const [aircraft, setAircraft] = useState<Aircraft>(AIRCRAFT[1]);

  const hubInfo = getAirportInfo(hub);
  const referenceKm = Math.round(aircraft.rangeKm * 0.75);
  const destinations = useMemo(
    () => DESTINATIONS.filter((code) => code !== hub),
    [hub],
  );
  const reachable = useMemo(
    () =>
      destinations.filter(
        (code) => greatCircleKm(hub, code) <= aircraft.rangeKm,
      ),
    [hub, aircraft, destinations],
  );

  return (
    <div className="grid grid-cols-1 min-h-[480px] w-full min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white lg:grid-cols-[300px_1fr]">
      <aside className="flex min-h-0 flex-col border-b border-slate-200 lg:border-r lg:border-b-0">
        <div className="border-b border-slate-200 bg-slate-50 px-5 py-3">
          <p className="flex items-center gap-2 font-mono text-[10px] tracking-widest text-slate-500">
            <Compass size={13} aria-hidden="true" /> RANGE EXPLORER
          </p>
        </div>
        <div className="border-b border-slate-200 p-4">
          <p className="text-[10px] tracking-widest text-slate-500 uppercase">
            Departure airport
          </p>
          <div
            role="group"
            aria-label="Hub airport"
            className="mt-1.5 flex rounded-lg border border-slate-200 bg-slate-50 p-0.5"
          >
            {HUBS.map((code) => (
              <button
                key={code}
                type="button"
                onClick={() => setHub(code)}
                aria-pressed={hub === code}
                className={`pressable flex-1 rounded-md px-2 py-1.5 font-mono text-xs font-medium ${
                  hub === code
                    ? "bg-slate-950 text-white"
                    : "text-slate-600 hover:text-slate-950"
                }`}
              >
                {code}
              </button>
            ))}
          </div>
          <p className="mt-2 truncate text-xs text-slate-500">
            {hubInfo?.name}
          </p>
        </div>
        <ul
          aria-label="Aircraft types"
          className="min-h-0 flex-1 overflow-y-auto p-2"
        >
          {AIRCRAFT.map((option) => {
            const isActive = aircraft.model === option.model;
            return (
              <li key={option.model}>
                <button
                  type="button"
                  onClick={() => setAircraft(option)}
                  aria-pressed={isActive}
                  className={`pressable flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left hover:bg-slate-100 ${
                    isActive ? "bg-orange-50" : ""
                  }`}
                >
                  <span className="min-w-0 flex-1">
                    <span
                      className={`block font-mono text-sm font-medium ${
                        isActive ? "text-orange-800" : "text-slate-950"
                      }`}
                    >
                      {option.model}
                    </span>
                    <span className="block text-xs text-slate-500">
                      {option.seats} seats
                    </span>
                  </span>
                  <span className="shrink-0 text-right font-mono text-[10px] text-slate-500">
                    {formatKm(option.rangeKm)}
                    <span className="mt-0.5 block font-sans">Max range</span>
                  </span>
                </button>
              </li>
            );
          })}
        </ul>
        <div role="status" className="border-t border-slate-200 px-4 py-4">
          <p className="text-sm font-medium text-slate-950">
            <span className="font-mono text-xl tabular-nums">
              {reachable.length}
            </span>
            <span className="text-slate-500"> / {destinations.length}</span>{" "}
            destinations in range
          </p>
          <p className="mt-1 text-xs leading-5 text-slate-500">
            Sample major airports within the orange boundary.
          </p>
        </div>
      </aside>
      <div className="flex min-w-0 flex-col">
        <div className="border-b border-slate-200 px-4 py-3">
          <p className="text-sm font-medium text-slate-950">
            {aircraft.model} range from <span className="font-mono">{hub}</span>
          </p>
          <p className="mt-1 text-xs text-slate-500">
            Both boundaries show distance from the same departure airport.
          </p>
        </div>
        <div className="relative min-h-[400px] flex-1 lg:min-h-[520px]">
          <Map
            theme="light"
            center={[hubInfo?.longitude ?? 121, hubInfo?.latitude ?? 25]}
            zoom={1.4}
            scrollZoom={false}
            renderWorldCopies
            className="absolute inset-0 h-full w-full"
          >
            <SingleWorldZoomLimit />
            <HubCamera hub={hub} />
            <FlightRange
              key={`${hub}-${aircraft.model}`}
              origin={hub}
              ranges={[
                {
                  distance: referenceKm,
                  color: REFERENCE_COLOR,
                  opacity: 0.09,
                },
                {
                  distance: aircraft.rangeKm,
                  color: MAX_RANGE_COLOR,
                  opacity: 0.06,
                },
              ]}
            />
          </Map>
        </div>
        <div className="border-t border-slate-200 bg-slate-50/80 p-4">
          <div className="flex items-center gap-4">
            <div
              aria-hidden="true"
              className="relative hidden size-20 shrink-0 items-center justify-center rounded-full border-2 bg-orange-50 sm:flex"
              style={{ borderColor: MAX_RANGE_COLOR }}
            >
              <div
                className="flex size-14 items-center justify-center rounded-full border-2 bg-sky-50"
                style={{ borderColor: REFERENCE_COLOR }}
              >
                <span className="size-2 rounded-full bg-slate-950" />
              </div>
            </div>
            <dl
              aria-label="Range boundary legend"
              className="min-w-0 flex-1 space-y-3"
            >
              <div>
                <dt className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs font-medium text-slate-950">
                  <span
                    aria-hidden="true"
                    className="h-0.5 w-4 shrink-0"
                    style={{ backgroundColor: REFERENCE_COLOR }}
                  />
                  75% reference
                  <span className="font-mono text-sky-700 tabular-nums">
                    {formatKm(referenceKm)}
                  </span>
                </dt>
                <dd className="mt-1 pl-6 text-xs leading-5 text-slate-500">
                  Inner boundary · three quarters of this aircraft’s maximum
                  range.
                </dd>
              </div>
              <div>
                <dt className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs font-medium text-slate-950">
                  <span
                    aria-hidden="true"
                    className="h-0.5 w-4 shrink-0"
                    style={{ backgroundColor: MAX_RANGE_COLOR }}
                  />
                  Maximum range
                  <span className="font-mono text-orange-800 tabular-nums">
                    {formatKm(aircraft.rangeKm)}
                  </span>
                </dt>
                <dd className="mt-1 pl-6 text-xs leading-5 text-slate-500">
                  Outer boundary · beyond this line is outside the selected
                  range.
                </dd>
              </div>
            </dl>
          </div>
          <p className="mt-3 border-t border-slate-200 pt-3 text-[11px] leading-5 text-slate-500">
            Distances follow Earth’s surface. The 75% line is a visual
            reference, not a fuel reserve. Actual reach varies with payload,
            winds and routing.
          </p>
        </div>
      </div>
    </div>
  );
}

More from flightcn

Browse all components in this library →