// Dependencies: react ^18, lucide-react
import React, { useEffect, useState } from 'react';
import { X, CreditCard, Check } from 'lucide-react';
function cn(...classes: (string | false | null | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
// ——— BottomSheet shell ———
type BottomSheetProps = {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
actions?: React.ReactNode;
};
export function BottomSheet({ open, onClose, title, children, actions }: BottomSheetProps) {
useEffect(() => {
if (!open) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [open, onClose]);
return (
<div
className={cn(
'fixed inset-0 z-50 transition-opacity duration-200',
open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
)}
role="dialog"
aria-modal="true"
aria-hidden={!open}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
{/* Sheet */}
<div
className={cn(
'absolute bottom-0 left-0 right-0 max-h-[80%] bg-zinc-900 border-t border-white/10 rounded-t-2xl shadow-2xl flex flex-col transition-transform duration-300 ease-out',
open ? 'translate-y-0' : 'translate-y-full'
)}
>
{/* Drag handle */}
<div className="flex justify-center pt-2.5 pb-1 shrink-0">
<div className="w-9 h-1 rounded-full bg-zinc-700" />
</div>
{/* Header */}
<div className="flex items-center justify-between px-5 pt-1 pb-3 border-b border-white/[0.08] shrink-0">
<h2 className="text-sm font-semibold text-white">{title}</h2>
<button
onClick={onClose}
aria-label="Close"
className="text-zinc-500 hover:text-zinc-200 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-5 py-4">{children}</div>
{/* Actions — items-stretch guarantees equal button heights */}
{actions && (
<div className="px-5 py-3 border-t border-white/[0.08] flex items-stretch gap-3 shrink-0">
{actions}
</div>
)}
</div>
</div>
);
}
// ——— Usage example: Payment method sheet ———
const METHODS = [
{ id: 'visa', label: 'Visa', last4: '4242', expires: '04/27', bg: '#0f2d5f', color: '#60a5fa' },
{ id: 'mc', label: 'Mastercard', last4: '5131', expires: '09/26', bg: '#5c1a08', color: '#f97316' },
{ id: 'paypal', label: 'PayPal', last4: null, expires: null, bg: '#0a2240', color: '#38bdf8' },
];
export function PaymentSheet() {
const [open, setOpen] = useState(true);
const [selected, setSelected] = useState('visa');
return (
<>
<button
onClick={() => setOpen(true)}
className="px-4 py-2 rounded-md text-sm font-semibold text-white bg-indigo-500 hover:bg-indigo-400 transition-colors"
>
Choose payment method
</button>
<BottomSheet
open={open}
onClose={() => setOpen(false)}
title="Payment method"
actions={
<>
<button
onClick={() => setOpen(false)}
className="px-4 py-2.5 rounded-md text-sm font-medium text-zinc-300 border border-white/[0.1] hover:bg-white/[0.05] transition-colors"
>
Cancel
</button>
<button
onClick={() => setOpen(false)}
className="flex-1 px-4 py-2.5 rounded-md text-sm font-semibold text-white bg-indigo-500 hover:bg-indigo-400 transition-colors"
>
Confirm
</button>
</>
}
>
<div className="flex flex-col gap-2">
{METHODS.map((m) => (
<button
key={m.id}
onClick={() => setSelected(m.id)}
className={cn(
'flex items-center gap-3 px-3.5 py-3 rounded-lg border transition-colors text-left w-full',
m.id === selected
? 'border-indigo-500/40 bg-indigo-500/10'
: 'border-white/[0.08] hover:bg-white/[0.04]'
)}
>
<div
className="w-8 h-8 rounded-md flex items-center justify-center shrink-0"
style={{ backgroundColor: m.bg }}
>
<CreditCard className="w-4 h-4" style={{ color: m.color }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white">
{m.label}{m.last4 ? ` ···· ${m.last4}` : ''}
</p>
{m.expires && (
<p className="text-xs text-zinc-500">Expires {m.expires}</p>
)}
</div>
{m.id === selected && (
<Check className="w-4 h-4 text-indigo-400 shrink-0" />
)}
</button>
))}
</div>
</BottomSheet>
</>
);
}