A few things that were technically interesting to build: The day/night terminator. It's not just a shaded half-sphere. I implemented a simplified USNO solar declination algorithm, computing the subsolar point for a given UTC timestamp, then tracing the great-circle terminator and sampling which side each pixel falls on. It renders pixel-by-pixel onto a Canvas overlay above the SVG map. During drag it bypasses React's render cycle entirely and draws directly on the canvas ref for zero-latency feedback. The map drag = time scrub. Dragging the map horizontally shifts time - one full map width equals 24 hours (1440 minutes). The math is straightforward (pixel delta → minute delta → new Date), but making it feel fluid required some care. City card updates use useDeferredValue so the main thread prioritizes the drag over re-rendering all the cards. URL state. The current city list, base city, and manual time are encoded in the URL and debounced at 500ms so you can share or bookmark a specific view. Restoring from URL has to deal with two city sources: ~306 hardcoded IANA cities and ~33k GeoNames cities that live in localStorage after a search. The GeoNames dataset was 33,334 cities after deduplication-by-timezone. I build a JSON file at deploy time rather than hitting the GeoNames API at runtime. Stack: React 18, TypeScript strict, Vite, Tailwind, Framer Motion, D3-geo Mercator, date-fns-tz, Hono for the city search API. MIT licensed.
Source: https://github.com/zzjoey/ZoneMap Live: https://zonemap.live
Known rough edges: mobile layout is functional but cramped, and the terminator calculation skips atmospheric refraction so it's off by ~0.5° at the poles.