"use client";
import * as React from "react";
import { AnimatePresence, MotionConfig, motion } from "motion/react";
import { cn } from "@/lib/utils";
import { defaultOsData, type AppId, type OsData, type WallpaperId } from "./data";
import { BootScreen, Spotlight } from "./sections/chrome";
import { Desktop, type WindowActions } from "./sections/desktop";
import { Phone } from "./sections/phone";
import { OsContext, Wallpaper, useIsNarrow, type OsApi } from "./sections/ui";
import { MIN_H, MIN_W, type Bounds, type WinState } from "./sections/window";
export type { OsData, AppId, WallpaperId };
export { defaultOsData };
export interface DesktopOsPortfolioTemplateProps {
/** Override any content group (person, apps, projects, tracks…). Missing groups use the demo content. */
data?: Partial<OsData>;
/** Apps opened right after boot on desktop. Default: About. */
initialApps?: AppId[];
initialWallpaper?: WallpaperId;
/** Skip the boot animation (it is always skipped with reduced motion). */
skipBoot?: boolean;
/** Called when the Contact app's form is submitted. */
onContact?: (msg: { name: string; email: string; message: string }) => Promise<void> | void;
/** Sizing is up to you — by default the OS fills the viewport. */
className?: string;
}
type Action =
| { type: "open"; id: AppId; bounds: Bounds; size: [number, number] }
| { type: "close"; id: AppId }
| { type: "focus"; id: AppId }
| { type: "minimize"; id: AppId }
| { type: "toggleMax"; id: AppId }
| { type: "commit"; id: AppId; g: { x: number; y: number; w: number; h: number } }
| { type: "fit"; bounds: Bounds }
| { type: "reset" };
type WmState = { wins: WinState[]; z: number; opened: number };
function wm(s: WmState, a: Action): WmState {
switch (a.type) {
case "open": {
const found = s.wins.find((w) => w.id === a.id);
if (found) return { ...s, z: s.z + 1, wins: s.wins.map((w) => (w.id === a.id ? { ...w, minimized: false, z: s.z + 1 } : w)) };
const w = Math.max(MIN_W, Math.min(a.size[0], a.bounds.w - 24));
const h = Math.max(MIN_H, Math.min(a.size[1], a.bounds.h - 16));
const off = (s.opened % 6) * 28;
const x = Math.max(8, Math.min(a.bounds.w - w - 8, Math.round((a.bounds.w - w) / 2) - 80 + off));
const y = Math.max(8, Math.min(a.bounds.h - h - 8, Math.round((a.bounds.h - h) / 2) - 30 + off));
return { z: s.z + 1, opened: s.opened + 1, wins: [...s.wins, { id: a.id, x, y, w, h, z: s.z + 1, minimized: false, maximized: false }] };
}
case "close":
return { ...s, wins: s.wins.filter((w) => w.id !== a.id) };
case "focus": {
const top = s.wins.reduce((m, w) => Math.max(m, w.z), 0);
const cur = s.wins.find((w) => w.id === a.id);
if (!cur || cur.z === top) return s;
return { ...s, z: s.z + 1, wins: s.wins.map((w) => (w.id === a.id ? { ...w, z: s.z + 1 } : w)) };
}
case "minimize":
return { ...s, wins: s.wins.map((w) => (w.id === a.id ? { ...w, minimized: true } : w)) };
case "toggleMax":
return { ...s, z: s.z + 1, wins: s.wins.map((w) => (w.id === a.id ? { ...w, maximized: !w.maximized, minimized: false, z: s.z + 1 } : w)) };
case "commit":
return { ...s, wins: s.wins.map((w) => (w.id === a.id ? { ...w, ...a.g } : w)) };
case "fit":
// Keep windows reachable when the desktop shrinks.
return {
...s,
wins: s.wins.map((w) => {
const nw = Math.min(w.w, Math.max(MIN_W, a.bounds.w - 16));
const nh = Math.min(w.h, Math.max(MIN_H, a.bounds.h - 8));
return { ...w, w: nw, h: nh, x: Math.max(0, Math.min(w.x, a.bounds.w - nw)), y: Math.max(0, Math.min(w.y, a.bounds.h - nh)) };
}),
};
case "reset":
return { wins: [], z: 0, opened: 0 };
}
}
export function DesktopOsPortfolioTemplate({ data, initialApps = ["about"], initialWallpaper = "dune", skipBoot = false, onContact, className }: DesktopOsPortfolioTemplateProps) {
const d = React.useMemo(() => ({ ...defaultOsData, ...data }), [data]);
const rootRef = React.useRef<HTMLDivElement>(null);
const narrow = useIsNarrow(rootRef);
const [booting, setBooting] = React.useState(!skipBoot);
const [bootKey, setBootKey] = React.useState(0);
const [wallpaper, setWallpaper] = React.useState<WallpaperId>(initialWallpaper);
const [state, dispatch] = React.useReducer(wm, { wins: [], z: 0, opened: 0 });
const [bounds, setBounds] = React.useState<Bounds>({ w: 1200, h: 700 });
const [phoneApp, setPhoneApp] = React.useState<AppId | null>(null);
const [spotlight, setSpotlight] = React.useState(false);
const [payload, setPayload] = React.useState<OsApi["payload"]>({});
const nonce = React.useRef(0);
const boundsRef = React.useRef(bounds);
const didInitial = React.useRef(false);
const initialRef = React.useRef(initialApps);
const onBounds = React.useCallback((b: Bounds) => {
boundsRef.current = b;
setBounds(b);
dispatch({ type: "fit", bounds: b });
}, []);
const open = React.useCallback(
(id: AppId, value?: string) => {
if (value !== undefined) setPayload((p) => ({ ...p, [id]: { value, n: ++nonce.current } }));
const app = d.apps.find((a) => a.id === id);
if (!app) return;
if (narrow) setPhoneApp(id);
else dispatch({ type: "open", id, bounds: boundsRef.current, size: app.size });
},
[d.apps, narrow],
);
const close = React.useCallback(
(id: AppId) => {
setPayload((p) => ({ ...p, [id]: undefined }));
if (narrow) setPhoneApp((cur) => (cur === id ? null : cur));
else dispatch({ type: "close", id });
},
[narrow],
);
const restart = React.useCallback(() => {
setSpotlight(false);
dispatch({ type: "reset" });
setPhoneApp(null);
didInitial.current = false;
setBootKey((k) => k + 1);
setBooting(true);
}, []);
// Open the initial apps once the desktop is ready.
React.useEffect(() => {
if (booting || narrow !== false || didInitial.current) return;
didInitial.current = true;
window.setTimeout(() => initialRef.current.forEach((a) => open(a)), 250);
}, [booting, narrow, open]);
// Global shortcut: Ctrl/⌘ K toggles Spotlight.
React.useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setSpotlight((s) => !s);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
const closeSpotlight = React.useCallback(() => setSpotlight(false), []);
const actions: WindowActions = React.useMemo(
() => ({
focus: (id) => dispatch({ type: "focus", id }),
close,
minimize: (id) => dispatch({ type: "minimize", id }),
toggleMax: (id) => dispatch({ type: "toggleMax", id }),
commit: (id, g) => dispatch({ type: "commit", id, g }),
launch: (id) => open(id),
}),
[close, open],
);
const api: OsApi = React.useMemo(
() => ({ data: d, mobile: !!narrow, wallpaper, setWallpaper, open, close, restart, openSpotlight: () => setSpotlight(true), payload, onContact }),
[d, narrow, wallpaper, open, close, restart, payload, onContact],
);
return (
<MotionConfig reducedMotion="user">
<OsContext.Provider value={api}>
<div
ref={rootRef}
data-os-root
data-wallpaper={wallpaper}
className={cn("relative isolate h-dvh min-h-[560px] w-full select-none overflow-hidden bg-black text-foreground antialiased [&_input]:select-text [&_textarea]:select-text", className)}
>
<AnimatePresence mode="wait">
<motion.div key={wallpaper} className="absolute inset-0" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.6 }}>
<Wallpaper id={wallpaper} />
</motion.div>
</AnimatePresence>
{narrow !== null && !booting && (
<motion.div className="absolute inset-0" initial={{ opacity: 0, scale: 1.02 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.6 }}>
{narrow ? (
<Phone app={phoneApp} onOpen={open} onHome={() => setPhoneApp(null)} onSpotlight={() => setSpotlight(true)} />
) : (
<Desktop wins={state.wins} actions={actions} bounds={bounds} onBounds={onBounds} onSpotlight={() => setSpotlight(true)} />
)}
</motion.div>
)}
<Spotlight open={spotlight && !booting} onClose={closeSpotlight} />
<AnimatePresence>{booting && <BootScreen key={bootKey} name={d.os.name} version={d.os.version} lines={d.os.bootLines} onDone={() => setBooting(false)} />}</AnimatePresence>
</div>
</OsContext.Provider>
</MotionConfig>
);
}