"use client"; import * as React from "react"; import { cn } from "./lib/utils"; /* ------------------------------------------------------------------------- * Context & Types * ----------------------------------------------------------------------- */ interface RadioCardsContextValue { value?: string; name?: string; disabled?: boolean; onSelect: (val: string) => void; registerItem: (id: string, element: HTMLDivElement | null) => void; } const RadioCardsContext = React.createContext(null); function useRadioCards() { const context = React.useContext(RadioCardsContext); if (!context) { throw new Error("RadioCard components must be used within a RadioCards provider."); } return context; } export interface RadioCardsProps extends React.HTMLAttributes { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; name?: string; disabled?: boolean; orientation?: "horizontal" | "vertical" | "grid"; columns?: 1 | 2 | 3 | 4; className?: string; children?: React.ReactNode; } /* ------------------------------------------------------------------------- * Root: RadioCards * ----------------------------------------------------------------------- */ export function RadioCards({ value, defaultValue, onValueChange, name, disabled = false, orientation = "grid", columns = 3, className, children, ...props }: RadioCardsProps) { const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue ?? ""); const isControlled = value !== undefined; const currentValue = isControlled ? value : uncontrolledValue; const generatedName = React.useId(); const groupName = name || generatedName; const itemElementsRef = React.useRef>(new Map()); const registerItem = React.useCallback((id: string, element: HTMLDivElement | null) => { if (element) { itemElementsRef.current.set(id, element); } else { itemElementsRef.current.delete(id); } }, []); const handleSelect = React.useCallback( (nextValue: string) => { if (disabled) return; if (!isControlled) { setUncontrolledValue(nextValue); } onValueChange?.(nextValue); }, [disabled, isControlled, onValueChange] ); const handleKeyDown = (event: React.KeyboardEvent) => { if (disabled) return; // Collect all non-disabled cards in DOM order const cardElements = Array.from( event.currentTarget.querySelectorAll('[role="radio"]:not([aria-disabled="true"])') ); if (cardElements.length === 0) return; const currentIndex = cardElements.findIndex((el) => el === document.activeElement); let nextIndex = currentIndex; switch (event.key) { case "ArrowDown": case "ArrowRight": event.preventDefault(); nextIndex = currentIndex < cardElements.length - 1 ? currentIndex + 1 : 0; break; case "ArrowUp": case "ArrowLeft": event.preventDefault(); nextIndex = currentIndex > 0 ? currentIndex - 1 : cardElements.length - 1; break; case "Home": event.preventDefault(); nextIndex = 0; break; case "End": event.preventDefault(); nextIndex = cardElements.length - 1; break; case " ": case "Enter": if (currentIndex >= 0 && currentIndex < cardElements.length) { event.preventDefault(); const target = cardElements[currentIndex]; const val = target.getAttribute("data-value"); if (val) handleSelect(val); } return; default: return; } const nextCard = cardElements[nextIndex]; if (nextCard) { nextCard.focus(); const val = nextCard.getAttribute("data-value"); if (val) { handleSelect(val); } } }; const orientationStyles = { vertical: "flex flex-col gap-4", horizontal: "flex flex-row flex-wrap gap-4", grid: cn( "grid gap-4", columns === 1 && "grid-cols-1", columns === 2 && "grid-cols-1 md:grid-cols-2", columns === 3 && "grid-cols-1 md:grid-cols-3", columns === 4 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" ), }; return (
{children}
); } /* ------------------------------------------------------------------------- * Item: RadioCard * ----------------------------------------------------------------------- */ export interface RadioCardProps extends React.HTMLAttributes { value: string; title?: string; subtitle?: string; tier?: string; price?: string; period?: string; badge?: string; badgeVariant?: "accent" | "default" | "secondary" | "destructive"; perks?: string[]; disabled?: boolean; soldOut?: boolean; notched?: boolean; serial?: string; className?: string; children?: React.ReactNode; } export function RadioCard({ value, title, subtitle, tier, price, period = "/pass", badge, badgeVariant = "accent", perks, disabled = false, soldOut = false, notched = true, serial, className, children, ...props }: RadioCardProps) { const { value: groupValue, disabled: groupDisabled, onSelect, registerItem } = useRadioCards(); const isChecked = groupValue === value; const isDisabled = disabled || groupDisabled || soldOut; const cardRef = React.useRef(null); React.useEffect(() => { registerItem(value, cardRef.current); return () => registerItem(value, null); }, [value, registerItem]); const handleClick = () => { if (!isDisabled) { onSelect(value); } }; const badgeVariantStyles = { accent: "border-accent text-accent bg-accent/10", default: "border-foreground text-foreground bg-foreground/10", secondary: "border-border text-muted-foreground bg-secondary", destructive: "border-destructive text-destructive bg-destructive/10", }; return (
{/* Inner Decorative Framing */}
{/* Ticket Semicircular Punch Notches */} {notched && ( <>