Modal
src/components/thegridcn/modal.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
description?: string;
footer?: React.ReactNode;
onClose: () => void;
open: boolean;
size?: "sm" | "md" | "lg";
title?: string;
}
export function Modal({
open,
onClose,
title,
description,
size = "md",
footer,
className,
children,
...props
}: ModalProps) {
// Close on Escape
React.useEffect(() => {
if (!open) {
return;
}
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onClose]);
// Lock body scroll
React.useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}
}, [open]);
if (!open) {
return null;
}
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-50 bg-background/60 backdrop-blur-sm"
onClick={onClose}
/>
<div
data-slot="tron-modal"
role="dialog"
aria-modal="true"
className={cn(
"fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded border border-primary/30 bg-card/95 shadow-[0_0_40px_rgba(var(--primary-rgb,0,180,255),0.08)] backdrop-blur-md",
size === "sm" && "w-full max-w-sm",
size === "md" && "w-full max-w-lg",
size === "lg" && "w-full max-w-2xl",
className
)}
{...props}
>
{/* Scanline */}
<div className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,0.03)_2px,rgba(0,0,0,0.03)_4px)]" />
{/* Header */}
{title || description ? (
<div className="border-primary/20 border-b px-5 py-3">
{title ? (
<h3 className="font-mono text-primary text-xs uppercase tracking-widest">
{title}
</h3>
) : null}
{description ? (
<p className="mt-0.5 font-mono text-[10px] text-foreground/40">
{description}
</p>
) : null}
</div>
) : null}
{/* Body */}
<div className="px-5 py-4">{children}</div>
{/* Footer */}
{footer ? (
<div className="flex items-center justify-end gap-2 border-primary/20 border-t px-5 py-3">
{footer}
</div>
) : null}
{/* Close button */}
<button
type="button"
onClick={onClose}
className="absolute top-3 right-3 flex h-6 w-6 items-center justify-center rounded text-foreground/30 transition-colors hover:bg-primary/10 hover:text-primary"
aria-label="Close"
>
<svg
aria-hidden="true"
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 1l8 8M9 1l-8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</button>
{/* Corner decorations */}
<div className="pointer-events-none absolute top-0 left-0 h-3 w-3 border-primary/50 border-t-2 border-l-2" />
<div className="pointer-events-none absolute top-0 right-0 h-3 w-3 border-primary/50 border-t-2 border-r-2" />
<div className="pointer-events-none absolute bottom-0 left-0 h-3 w-3 border-primary/50 border-b-2 border-l-2" />
<div className="pointer-events-none absolute right-0 bottom-0 h-3 w-3 border-primary/50 border-r-2 border-b-2" />
</div>
</>
);
}
/** Reusable button for modal footers */
export function ModalButton({
variant = "default",
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "default" | "primary" | "danger";
}) {
return (
<button
type="button"
className={cn(
"rounded border px-4 py-1.5 font-mono text-[10px] uppercase tracking-widest transition-all",
variant === "default" &&
"border-primary/20 text-foreground/50 hover:bg-primary/5 hover:text-foreground/70",
variant === "primary" &&
"border-primary/40 bg-primary/10 text-primary hover:bg-primary/20",
variant === "danger" &&
"border-red-500/40 bg-red-500/10 text-red-400 hover:bg-red-500/20",
className
)}
{...props}
/>
);
}
- Interaction
- modal