"use client";
import { useState } from "react";
import { defineRegistry, useStateBinding } from "@json-render/react";
import { toast } from "sonner";
import { playgroundCatalog } from "./catalog";
// shadcn components
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import {
Dialog as DialogPrimitive,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Accordion as AccordionPrimitive,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Carousel as CarouselPrimitive,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Table as TablePrimitive,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Drawer as DrawerPrimitive,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import {
DropdownMenu as DropdownMenuPrimitive,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Pagination as PaginationPrimitive,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Popover as PopoverPrimitive,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Skeleton } from "@/components/ui/skeleton";
import { Slider } from "@/components/ui/slider";
import {
Tabs as TabsPrimitive,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Toggle } from "@/components/ui/toggle";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
Tooltip as TooltipPrimitive,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
// =============================================================================
// Registry — components + actions, types inferred from catalog
// =============================================================================
export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
components: {
// ── Layout ────────────────────────────────────────────────────────
Card: ({ props, children }) => {
const maxWidthClass =
props.maxWidth === "sm"
? "max-w-xs sm:min-w-[280px]"
: props.maxWidth === "md"
? "max-w-sm sm:min-w-[320px]"
: props.maxWidth === "lg"
? "max-w-md sm:min-w-[360px]"
: "w-full";
const centeredClass = props.centered ? "mx-auto" : "";
return (
{(props.title || props.description) && (
{props.title && (
{props.title}
)}
{props.description && (
{props.description}
)}
)}
{children}
);
},
Stack: ({ props, children }) => {
const isHorizontal = props.direction === "horizontal";
const gapClass =
props.gap === "lg"
? "gap-4"
: props.gap === "md"
? "gap-3"
: props.gap === "sm"
? "gap-2"
: props.gap === "none"
? "gap-0"
: "gap-3";
const alignClass =
props.align === "center"
? "items-center"
: props.align === "end"
? "items-end"
: props.align === "stretch"
? "items-stretch"
: "items-start";
const justifyClass =
props.justify === "center"
? "justify-center"
: props.justify === "end"
? "justify-end"
: props.justify === "between"
? "justify-between"
: props.justify === "around"
? "justify-around"
: "";
return (
{children}
);
},
Grid: ({ props, children }) => {
const n = props.columns ?? 1;
const cols =
n >= 6
? "grid-cols-6"
: n >= 5
? "grid-cols-5"
: n >= 4
? "grid-cols-4"
: n >= 3
? "grid-cols-3"
: n >= 2
? "grid-cols-2"
: "grid-cols-1";
const gridGap =
props.gap === "lg" ? "gap-4" : props.gap === "sm" ? "gap-2" : "gap-3";
return {children}
;
},
Separator: ({ props }) => (
),
Tabs: ({ props, emit }) => {
const tabs = props.tabs ?? [];
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState(
props.defaultValue ?? tabs[0]?.value ?? "",
);
const value = props.statePath
? (boundValue ?? tabs[0]?.value ?? "")
: localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{
setValue(v);
emit?.("change");
}}
>
{tabs.map((tab) => (
{tab.label}
))}
);
},
Accordion: ({ props }) => {
const items = props.items ?? [];
const accordionType = props.type ?? "single";
if (accordionType === "multiple") {
return (
{items.map((item, i) => (
{item.title}
{item.content}
))}
);
}
return (
{items.map((item, i) => (
{item.title}
{item.content}
))}
);
},
Collapsible: ({ props, children }) => {
const [open, setOpen] = useState(props.defaultOpen ?? false);
return (
{children}
);
},
Dialog: ({ props, children }) => {
const [open, setOpen] = useStateBinding(props.openPath);
return (
setOpen(v)}>
{props.title}
{props.description && (
{props.description}
)}
{children}
);
},
Drawer: ({ props, children }) => {
const [open, setOpen] = useStateBinding(props.openPath);
return (
setOpen(v)}>
{props.title}
{props.description && (
{props.description}
)}
{children}
);
},
Carousel: ({ props }) => {
const items = props.items ?? [];
return (
{items.map((item, i) => (
{item.title && (
{item.title}
)}
{item.description && (
{item.description}
)}
))}
);
},
// ── Data Display ──────────────────────────────────────────────────
Table: ({ props }) => {
const columns = props.columns ?? [];
const rawRows: unknown[] = Array.isArray(props.rows) ? props.rows : [];
const rows = rawRows.map((row) => {
if (Array.isArray(row)) return row.map(String);
if (row && typeof row === "object") {
const obj = row as Record;
return columns.map((col) =>
String(obj[col] ?? obj[col.toLowerCase()] ?? ""),
);
}
return columns.map(() => "");
});
return (
{props.caption && {props.caption}}
{columns.map((col) => (
{col}
))}
{rows.map((row, i) => (
{row.map((cell, j) => (
{cell}
))}
))}
);
},
Heading: ({ props }) => {
const level = props.level ?? "h2";
const headingClass =
level === "h1"
? "text-2xl font-bold"
: level === "h3"
? "text-base font-semibold"
: level === "h4"
? "text-sm font-semibold"
: "text-lg font-semibold";
if (level === "h1")
return {props.text}
;
if (level === "h3")
return {props.text}
;
if (level === "h4")
return {props.text}
;
return {props.text}
;
},
Text: ({ props }) => {
const textClass =
props.variant === "caption"
? "text-xs"
: props.variant === "muted"
? "text-sm text-muted-foreground"
: props.variant === "lead"
? "text-xl text-muted-foreground"
: props.variant === "code"
? "font-mono text-sm bg-muted px-1.5 py-0.5 rounded"
: "text-sm";
if (props.variant === "code") {
return {props.text};
}
return {props.text}
;
},
Image: ({ props }) => (
{props.alt || "img"}
),
Avatar: ({ props }) => {
const name = props.name || "?";
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase();
const avatarSize =
props.size === "lg"
? "w-12 h-12 text-base"
: props.size === "sm"
? "w-8 h-8 text-xs"
: "w-10 h-10 text-sm";
return (
{initials}
);
},
Badge: ({ props }) => {
const variant =
props.variant === "success" || props.variant === "warning"
? "secondary"
: props.variant === "danger"
? "destructive"
: "default";
const customClass =
props.variant === "success"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100"
: props.variant === "warning"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100"
: "";
return (
{props.text}
);
},
Alert: ({ props }) => {
const variant = props.type === "error" ? "destructive" : "default";
const customClass =
props.type === "success"
? "border-green-200 bg-green-50 text-green-900 dark:border-green-800 dark:bg-green-950 dark:text-green-100"
: props.type === "warning"
? "border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100"
: props.type === "info"
? "border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100"
: "";
return (
{props.title}
{props.message && (
{props.message}
)}
);
},
Progress: ({ props }) => {
const value = Math.min(100, Math.max(0, props.value || 0));
return (
{props.label && (
)}
);
},
Skeleton: ({ props }) => (
),
Spinner: ({ props }) => {
const sizeClass =
props.size === "lg"
? "h-8 w-8"
: props.size === "sm"
? "h-4 w-4"
: "h-6 w-6";
return (
{props.label && (
{props.label}
)}
);
},
Tooltip: ({ props }) => (
{props.text}
{props.content}
),
Popover: ({ props }) => (
{props.content}
),
Rating: ({ props }) => {
const ratingValue = props.value || 0;
const maxRating = props.max ?? 5;
return (
{props.label && (
)}
{Array.from({ length: maxRating }).map((_, i) => (
*
))}
);
},
// ── Charts ────────────────────────────────────────────────────────
BarGraph: ({ props }) => {
const data = props.data || [];
const maxValue = Math.max(...data.map((d) => d.value), 1);
return (
{props.title && (
{props.title}
)}
{data.map((d, i) => (
))}
);
},
LineGraph: ({ props }) => {
const data = props.data || [];
const maxValue = Math.max(...data.map((d) => d.value));
const minValue = Math.min(...data.map((d) => d.value));
const range = maxValue - minValue || 1;
const width = 300;
const height = 100;
const padding = { top: 10, right: 10, bottom: 10, left: 10 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const points = data.map((d, i) => {
const x =
padding.left +
(data.length > 1
? (i / (data.length - 1)) * chartWidth
: chartWidth / 2);
const y =
padding.top +
chartHeight -
((d.value - minValue) / range) * chartHeight;
return { x, y, ...d };
});
const pathD =
points.length > 0
? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}`
: "";
return (
{props.title && (
{props.title}
)}
{data.length > 0 && (
{data.map((d, i) => (
{d.label}
))}
)}
);
},
// ── Form Inputs ───────────────────────────────────────────────────
Input: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState("");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{props.label && }
setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") emit?.("submit");
}}
onFocus={() => emit?.("focus")}
onBlur={() => emit?.("blur")}
/>
);
},
Textarea: ({ props }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState("");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{props.label && }
);
},
Select: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState("");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
const options = props.options ?? [];
return (
);
},
Checkbox: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localChecked, setLocalChecked] = useState(!!props.checked);
const checked = props.statePath ? (boundValue ?? false) : localChecked;
const setChecked = props.statePath ? setBoundValue! : setLocalChecked;
return (
{
setChecked(c === true);
emit?.("change");
}}
/>
);
},
Radio: ({ props, emit }) => {
const options = props.options ?? [];
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState(options[0] ?? "");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{props.label &&
}
{
setValue(v);
emit?.("change");
}}
>
{options.map((opt) => (
))}
);
},
Switch: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localChecked, setLocalChecked] = useState(!!props.checked);
const checked = props.statePath ? (boundValue ?? false) : localChecked;
const setChecked = props.statePath ? setBoundValue! : setLocalChecked;
return (
{
setChecked(c);
emit?.("change");
}}
/>
);
},
Slider: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState(props.min ?? 0);
const value = props.statePath
? (boundValue ?? props.min ?? 0)
: localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{props.label && (
{value}
)}
{
setValue(v[0] ?? 0);
emit?.("change");
}}
/>
);
},
// ── Actions ───────────────────────────────────────────────────────
Button: ({ props, emit }) => {
const variant =
props.variant === "danger"
? "destructive"
: props.variant === "secondary"
? "secondary"
: "default";
return (
);
},
Link: ({ props, emit }) => (
),
DropdownMenu: ({ props, emit }) => {
const items = props.items ?? [];
return (
{items.map((item) => (
emit?.("select")}
>
{item.label}
))}
);
},
Toggle: ({ props, emit }) => {
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localPressed, setLocalPressed] = useState(props.pressed ?? false);
const pressed = props.statePath ? (boundValue ?? false) : localPressed;
const setPressed = props.statePath ? setBoundValue! : setLocalPressed;
return (
{
setPressed(v);
emit?.("change");
}}
>
{props.label}
);
},
ToggleGroup: ({ props, emit }) => {
const type = props.type ?? "single";
const items = props.items ?? [];
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState(items[0]?.value ?? "");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
if (type === "multiple") {
return (
{items.map((item) => (
{item.label}
))}
);
}
return (
{
if (v) {
setValue(v);
emit?.("change");
}
}}
>
{items.map((item) => (
{item.label}
))}
);
},
ButtonGroup: ({ props, emit }) => {
const buttons = props.buttons ?? [];
const [boundValue, setBoundValue] = props.statePath
? useStateBinding(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
const [localValue, setLocalValue] = useState(buttons[0]?.value ?? "");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
return (
{buttons.map((btn, i) => (
))}
);
},
Pagination: ({ props, emit }) => {
const [boundValue, setBoundValue] = useStateBinding(
props.statePath,
);
const currentPage = boundValue ?? 1;
const pages = Array.from({ length: props.totalPages }, (_, i) => i + 1);
return (
{
e.preventDefault();
if (currentPage > 1) {
setBoundValue(currentPage - 1);
emit?.("change");
}
}}
/>
{pages.map((page) => (
{
e.preventDefault();
setBoundValue(page);
emit?.("change");
}}
>
{page}
))}
{
e.preventDefault();
if (currentPage < props.totalPages) {
setBoundValue(currentPage + 1);
emit?.("change");
}
}}
/>
);
},
},
actions: {
// Built-in state actions — handled by ActionProvider, stubs needed for types
setState: async () => {},
pushState: async () => {},
removeState: async () => {},
// Demo actions — show toasts
buttonClick: async (params) => {
const message = (params?.message as string) || "Button clicked!";
toast.success(message);
},
formSubmit: async (params) => {
const formName = (params?.formName as string) || "Form";
toast.success(`${formName} submitted successfully!`);
},
linkClick: async (params) => {
const href = (params?.href as string) || "#";
toast.info(`Navigating to: ${href}`);
},
},
});
// Fallback component for unknown types
export function Fallback({ type }: { type: string }) {
return [{type}]
;
}