import React, { useEffect, useMemo, useState } from “react”;
import { motion } from “framer-motion”;
import { Card, CardContent, CardHeader, CardTitle } from “@/components/ui/card”;
import { Button } from “@/components/ui/button”;
import { Input } from “@/components/ui/input”;
import { Badge } from “@/components/ui/badge”;
import { Tabs, TabsContent, TabsList, TabsTrigger } from “@/components/ui/tabs”;
import { MapPin, Sunrise, Sunset, MoonStar, Orbit, CalendarDays, Compass } from “lucide-react”;
const KEMETIC_MONTHS = [
{ name: “Tekhi”, season: “Akhet”, seasonMeaning: “the growing / harvest” },
{ name: “Menkhet”, season: “Akhet”, seasonMeaning: “the growing / harvest” },
{ name: “HetHeru”, season: “Akhet”, seasonMeaning: “the growing / harvest” },
{ name: “Ka Ka”, season: “Akhet”, seasonMeaning: “the growing / harvest” },
{ name: “ShefBdet”, season: “Shemu”, seasonMeaning: “the waters” },
{ name: “Rekh Netch”, season: “Shemu”, seasonMeaning: “the waters” },
{ name: “Rekh Wr”, season: “Shemu”, seasonMeaning: “the waters” },
{ name: “Renunet”, season: “Shemu”, seasonMeaning: “the waters” },
{ name: “Khonsu”, season: “Peret”, seasonMeaning: “the planting and growing” },
{ name: “Khenti”, season: “Peret”, seasonMeaning: “the planting and growing” },
{ name: “Ipet”, season: “Peret”, seasonMeaning: “the planting and growing” },
{ name: “MesRa”, season: “Peret”, seasonMeaning: “the planting and growing” },
];
const EPAGOMENAL_LABEL = “Heriu Renpet”;
const SYNODIC_MONTH = 29.530588853;
const NEW_MOON_REF_UTC = Date.parse(“2000-01-06T18:14:00Z”);
const SIDEREAL_DAY_HOURS = 23.9344696;
function pad(n) {
return String(n).padStart(2, “0”);
}
function dayOfYear(date) {
const start = new Date(date.getFullYear(), 0, 0);
const diff = date – start;
const oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
}
function getKemeticYearAnchor(date) {
const y = date.getFullYear();
const thisYearAnchor = new Date(y, 11, 25, 0, 0, 0, 0);
return date >= thisYearAnchor ? thisYearAnchor : new Date(y – 1, 11, 25, 0, 0, 0, 0);
}
function getKemeticDate(date) {
const anchor = getKemeticYearAnchor(date);
const msPerDay = 1000 * 60 * 60 * 24;
const offset = Math.floor((new Date(date.getFullYear(), date.getMonth(), date.getDate()) – anchor) / msPerDay);
if (offset >= 360) {
return {
type: “epagomenal”,
day: offset – 359,
monthIndex: null,
monthName: EPAGOMENAL_LABEL,
season: “Threshold Days”,
seasonMeaning: “year transition”,
anchor,
dayOfCycle: offset + 1,
};
}
const monthIndex = Math.floor(offset / 30);
const day = (offset % 30) + 1;
const month = KEMETIC_MONTHS[monthIndex];
return {
type: “month”,
day,
monthIndex,
monthName: month.name,
season: month.season,
seasonMeaning: month.seasonMeaning,
anchor,
dayOfCycle: offset + 1,
};
}
function julianDate(date) {
return date.getTime() / 86400000 + 2440587.5;
}
function getGMSTHours(date) {
const jd = julianDate(date);
const T = (jd – 2451545.0) / 36525.0;
let gmst = 280.46061837 + 360.98564736629 * (jd – 2451545) + 0.000387933 * T * T – (T * T * T) / 38710000;
gmst = ((gmst % 360) + 360) % 360;
return gmst / 15;
}
function getLocalSiderealTime(date, longitude) {
const gmst = getGMSTHours(date);
let lst = gmst + longitude / 15;
lst = ((lst % 24) + 24) % 24;
const h = Math.floor(lst);
const m = Math.floor((lst – h) * 60);
const s = Math.floor((((lst – h) * 60) – m) * 60);
return { decimalHours: lst, text: `${pad(h)}:${pad(m)}:${pad(s)}` };
}
function getMoonData(date) {
const days = (date.getTime() – NEW_MOON_REF_UTC) / 86400000;
const age = ((days % SYNODIC_MONTH) + SYNODIC_MONTH) % SYNODIC_MONTH;
const illumination = (1 – Math.cos((2 * Math.PI * age) / SYNODIC_MONTH)) / 2;
let phaseName = “New Moon”;
if (age >= 1.84566 && age < 5.53699) phaseName = "Waxing Crescent";
else if (age >= 5.53699 && age < 9.22831) phaseName = "First Quarter";
else if (age >= 9.22831 && age < 12.91963) phaseName = "Waxing Gibbous";
else if (age >= 12.91963 && age < 16.61096) phaseName = "Full Moon";
else if (age >= 16.61096 && age < 20.30228) phaseName = "Waning Gibbous";
else if (age >= 20.30228 && age < 23.99361) phaseName = "Last Quarter";
else if (age >= 23.99361 && age < 27.68493) phaseName = "Waning Crescent";
const siderealIndex = Math.floor(age) % 27;
return {
age,
illumination,
phaseName,
siderealLunarDay: siderealIndex + 1,
};
}
function zodiacFromSolarLongitude(dayIndex) {
const signs = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
];
const signIndex = Math.floor((((dayIndex - 80 + 365) % 365) / 30.4167)) % 12;
return signs[signIndex];
}
function visibleSeasonalConstellations(dayIndex) {
const wheel = [
["Orion", "Taurus", "Auriga"],
["Gemini", "Canis Major", "Monoceros"],
["Leo", "Cancer", "Hydra"],
["Virgo", "Boötes", "Corvus"],
["Libra", "Scorpius", "Ophiuchus"],
["Sagittarius", "Scutum", "Lyra"],
["Cygnus", "Aquila", "Hercules"],
["Pegasus", "Aquarius", "Capricornus"],
["Pisces", "Andromeda", "Cassiopeia"],
["Aries", "Perseus", "Triangulum"],
["Taurus", "Orion", "Eridanus"],
["Gemini", "Canis Minor", "Auriga"],
];
return wheel[Math.floor(((dayIndex % 365) / 365) * 12) % 12];
}
function formatTime(date, locale = navigator.language || "en-US") {
return new Intl.DateTimeFormat(locale, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
}).format(date);
}
function formatDate(date, locale = navigator.language || "en-US") {
return new Intl.DateTimeFormat(locale, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
}
function buildMonthGrid(currentDate) {
const k = getKemeticDate(currentDate);
if (k.type !== "month") return [];
return Array.from({ length: 30 }, (_, i) => i + 1);
}
function App() {
const [now, setNow] = useState(new Date());
const [coords, setCoords] = useState({ lat: 33.749, lon: -84.388, label: “Atlanta, GA” });
const [manualLat, setManualLat] = useState(“33.749”);
const [manualLon, setManualLon] = useState(“-84.388”);
const [locationReady, setLocationReady] = useState(false);
const [sunData, setSunData] = useState(null);
const [error, setError] = useState(“”);
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
if (!navigator.geolocation) {
setLocationReady(true);
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
setCoords({ lat, lon, label: “Current Location” });
setManualLat(lat.toFixed(4));
setManualLon(lon.toFixed(4));
setLocationReady(true);
},
() => setLocationReady(true),
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 600000 }
);
}, []);
useEffect(() => {
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
const controller = new AbortController();
async function loadSun() {
try {
const url = `https://api.sunrise-sunset.org/json?lat=${coords.lat}&lng=${coords.lon}&formatted=0&date=${dateStr}`;
const res = await fetch(url, { signal: controller.signal });
const data = await res.json();
if (data.status === “OK”) {
setSunData({
sunrise: new Date(data.results.sunrise),
sunset: new Date(data.results.sunset),
solarNoon: new Date(data.results.solar_noon),
dayLength: data.results.day_length,
});
}
} catch {
// silent fallback
}
}
loadSun();
return () => controller.abort();
}, [coords, now.getFullYear(), now.getMonth(), now.getDate()]);
const kemetic = useMemo(() => getKemeticDate(now), [now]);
const sidereal = useMemo(() => getLocalSiderealTime(now, coords.lon), [now, coords.lon]);
const moon = useMemo(() => getMoonData(now), [now]);
const dayIndex = useMemo(() => dayOfYear(now), [now]);
const solarSign = useMemo(() => zodiacFromSolarLongitude(dayIndex), [dayIndex]);
const seasonalConstellations = useMemo(() => visibleSeasonalConstellations(dayIndex), [dayIndex]);
const monthGrid = useMemo(() => buildMonthGrid(now), [now]);
function applyManualLocation() {
const lat = parseFloat(manualLat);
const lon = parseFloat(manualLon);
if (Number.isNaN(lat) || Number.isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
setError(“Enter a valid latitude and longitude.”);
return;
}
setError(“”);
setCoords({ lat, lon, label: “Manual Coordinates” });
}
return (
Kemetic Calendar
Local civil time mapped into your Kemetic year beginning on December 25 with sidereal, lunar, and solar context.
{formatTime(now)}
{formatDate(now)}
Kemetic Date
{kemetic.type === “month” ? `Day ${kemetic.day} of ${kemetic.monthName}` : `${EPAGOMENAL_LABEL} Day ${kemetic.day}`}
{kemetic.season}
{kemetic.seasonMeaning}
Local Sidereal Time
{sidereal.text}
Measures the sky’s stellar rotation overhead at your longitude.
Lunar Context
{moon.phaseName}
Age: {moon.age.toFixed(1)} days · Illumination: {(moon.illumination * 100).toFixed(0)}% · Sidereal lunar day: {moon.siderealLunarDay}
Sky Notes
Solar sign zone: {solarSign}
Seasonal constellations: {seasonalConstellations.join(“, “)}
This is an observational sky guide, meant as a quick local reference rather than a full planetarium model.
Location
{coords.label}: {coords.lat.toFixed(4)}, {coords.lon.toFixed(4)}
setManualLat(e.target.value)} placeholder=”Latitude” className=”rounded-xl bg-slate-900 border-slate-700″ />
setManualLon(e.target.value)} placeholder=”Longitude” className=”rounded-xl bg-slate-900 border-slate-700″ />
{error && {error}
}
{!locationReady && Attempting to read device location…
}
Sunrise / Sunset
Sunrise
{sunData ? formatTime(sunData.sunrise) : “—”}
Sunset
{sunData ? formatTime(sunData.sunset) : “—”}
Solar Noon
{sunData ? formatTime(sunData.solarNoon) : “—”}
Day Length
{sunData?.dayLength || “—”}
Month View
Year Wheel
Reference
Current Kemetic Month
{kemetic.type === “month” ? (
<>
{kemetic.monthName} · {kemetic.season} · {kemetic.seasonMeaning}
{monthGrid.map((d) => (
Day
{d}
))}
>
) : (
You are in {EPAGOMENAL_LABEL}, the 5 transitional days between MesRa and Tekhi.
)}
Kemetic Year Structure
{[
{ season: “Akhet”, meaning: “the growing / harvest”, months: KEMETIC_MONTHS.slice(0, 4) },
{ season: “Shemu”, meaning: “the waters”, months: KEMETIC_MONTHS.slice(4, 8) },
{ season: “Peret”, meaning: “the planting and growing”, months: KEMETIC_MONTHS.slice(8, 12) },
].map((group) => (
{group.season}
{group.meaning}
{group.months.map((m, i) => (
{m.name}
Month {i + 1 + (group.season === “Shemu” ? 4 : group.season === “Peret” ? 8 : 0)}
))}
))}
Year Threshold
Days 361–365 are shown as {EPAGOMENAL_LABEL}, transitional year-ending days before Tekhi begins again on December 25.
How this prototype works
Kemetic year: Starts on December 25 as you specified, with Tekhi as day 1.
Month system: 12 months of 30 days each, followed by 5 transition days.
Lunar data: Uses a mathematical lunar phase model to estimate moon age, phase, illumination, and a simple sidereal lunar day index.
Sidereal time: Calculated from your current UTC time and longitude to show the stellar clock overhead.
Constellations: Displayed as a practical observational guide tied to the time of year, not a full star-catalog engine.
Sunrise / sunset: Pulled live for your local coordinates from a public sunrise-sunset endpoint and displayed in local device time.
);
}
export default App;