Falnix UI

Unified Modal System

A single architectural primitive for all overlay interactions. Handles physics, trapping, and layout constraints automatically.

Modal is constrained to the preview canvas.

Live Canvas

Application Content Layer

Installation

This robust system handles all layout variants. Ensure you have `framer-motion` installed. Add this to `components/ui/modal.tsx`.

1"use client";
2
3import React from "react";
4import { X } from "lucide-react";
5import { motion, AnimatePresence } from "framer-motion";
6import { cn } from "@/lib/utils";
7
8// --- Types ---
9export type ModalPlacement = "center" | "top" | "bottom" | "left" | "right" | "fullscreen";
10export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
11
12interface ModalContextType {
13 onClose: () => void;
14}
15const ModalContext = React.createContext<ModalContextType | undefined>(undefined);
16
17interface ModalProps {
18 open: boolean;
19 onOpenChange: (open: boolean) => void;
20 children: React.ReactNode;
21 placement?: ModalPlacement;
22 size?: ModalSize;
23 backdrop?: "blur" | "dark" | "none";
24 className?: string;
25}
26
27// --- Main Component ---
28export const Modal = ({
29 open,
30 onOpenChange,
31 children,
32 placement = "center",
33 size = "md",
34 backdrop = "blur",
35 className,
36}: ModalProps) => {
37
38 const placementStyles = {
39 center: "items-center justify-center p-4",
40 top: "items-start justify-center p-4 pt-10",
41 bottom: "items-end justify-center p-4 pb-0 md:pb-10",
42 left: "items-center justify-start",
43 right: "items-center justify-end",
44 fullscreen: "items-center justify-center",
45 };
46
47 const animationVariants = {
48 center: { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.95 } },
49 top: { initial: { opacity: 0, y: -20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 } },
50 bottom: { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 } },
51 left: { initial: { opacity: 0, x: -20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: -20 } },
52 right: { initial: { opacity: 0, x: 20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: 20 } },
53 fullscreen: { initial: { opacity: 0, scale: 0.98 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.98 } },
54 };
55
56 const sizeClasses = {
57 sm: "max-w-sm",
58 md: "max-w-md",
59 lg: "max-w-2xl",
60 xl: "max-w-4xl",
61 full: "max-w-full h-full",
62 };
63
64 const isSheet = placement === "left" || placement === "right";
65 const isFullscreen = placement === "fullscreen";
66
67 return (
68 <ModalContext.Provider value={{ onClose: () => onOpenChange(false) }}>
69 <AnimatePresence>
70 {open && (
71 <div
72 className={cn(
73 "fixed inset-0 z-50 flex overflow-hidden",
74 placementStyles[placement],
75 className
76 )}
77 role="dialog"
78 aria-modal="true"
79 >
80 {/* Backdrop */}
81 {backdrop !== "none" && (
82 <motion.div
83 initial={{ opacity: 0 }}
84 animate={{ opacity: 1 }}
85 exit={{ opacity: 0 }}
86 onClick={() => onOpenChange(false)}
87 className={cn(
88 "absolute inset-0 z-0",
89 backdrop === "blur" ? "bg-black/40 backdrop-blur-sm" : "bg-black/60"
90 )}
91 />
92 )}
93
94 {/* Content */}
95 <motion.div
96 variants={animationVariants[placement]}
97 initial="initial"
98 animate="animate"
99 exit="exit"
100 transition={{ type: "spring", stiffness: 350, damping: 25 }}
101 className={cn(
102 "relative z-10 flex flex-col bg-[#0F0F10] border border-white/10 shadow-2xl overflow-hidden",
103 isSheet || isFullscreen ? "h-full rounded-none" : "rounded-xl max-h-[90%] w-full",
104 !isFullscreen && !isSheet ? sizeClasses[size] : "",
105 isSheet ? (size === "sm" ? "w-72" : "w-80 md:w-96") : "",
106 isSheet && placement === "right" ? "border-l" : "",
107 isSheet && placement === "left" ? "border-r" : "",
108 isFullscreen ? "w-full h-full border-none" : ""
109 )}
110 >
111 {children}
112 </motion.div>
113 </div>
114 )}
115 </AnimatePresence>
116 </ModalContext.Provider>
117 );
118};
119
120// --- Subcomponents ---
121
122export const ModalHeader = ({ children, className }: { children: React.ReactNode; className?: string }) => {
123 const ctx = React.useContext(ModalContext);
124 return (
125 <div className={cn("px-6 py-4 border-b border-white/5 flex items-center justify-between shrink-0 bg-[#0F0F10]", className)}>
126 <div className="font-semibold text-white">{children}</div>
127 <button onClick={ctx?.onClose} className="text-neutral-500 hover:text-white transition-colors">
128 <X className="w-4 h-4" />
129 </button>
130 </div>
131 );
132};
133
134export const ModalBody = ({ children, className }: { children: React.ReactNode; className?: string }) => {
135 return (
136 <div className={cn("p-6 overflow-y-auto flex-1 bg-[#0A0A0A]", className)}>
137 {children}
138 </div>
139 );
140};
141
142export const ModalFooter = ({ children, className }: { children: React.ReactNode; className?: string }) => {
143 return (
144 <div className={cn("px-6 py-4 border-t border-white/5 bg-[#0F0F10] shrink-0 flex items-center justify-end gap-3", className)}>
145 {children}
146 </div>
147 );
148};