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;
error: Content is protected !!