shadimage
buzzform
uiform-buildersource: ok · hit

FormBuilder

Visual drag-and-drop form builder primitives for BuzzForm v2.

npx shadcn add "https://form.buildnbuzz.com/r/form-builder.json"
free

Interaction

drag

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@buildnbuzz/form-core@buildnbuzz/form-react@buildnbuzz/form-builder-core@buildnbuzz/form-builder-react@dnd-kit/core@dnd-kit/sortable@dnd-kit/utilitiesfumadocs-uizustand

Registry dependencies

@buzzform/allalertbadgebuttoncardcheckboxcollapsibledialogemptyfieldinputscroll-areaselectseparatorsheetsidebarsonnerspinnerswitchtabstextareatooltip

Files27 · source cached 10/8/2026, 2:57:18 PM

registry/shadcn/builder/canvas/canvas-toolbar.tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
  TooltipProvider,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useBuilderStore, useUndoRedo } from "@buildnbuzz/form-builder-react";
import { IconPlaceholder } from "@/components/icon-placeholder";

/**
 * Floating canvas toolbar at bottom-center of the canvas area.
 * Provides undo/redo, mode toggle, viewport switching, and zoom controls.
 */
export function CanvasToolbar() {
  const viewport = useBuilderStore((state) => state.viewport);
  const setViewport = useBuilderStore((state) => state.setViewport);
  const zoom = useBuilderStore((state) => state.zoom);
  const setZoom = useBuilderStore((state) => state.setZoom);
  const mode = useBuilderStore((state) => state.mode);
  const setMode = useBuilderStore((state) => state.setMode);
  const { undo, redo, canUndo, canRedo } = useUndoRedo();

  const isPreview = mode === "preview";

  return (
    <div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex items-center gap-1 p-1.5 rounded-full border border-border/50 bg-card/80 backdrop-blur-xl shadow-xl shadow-black/5 supports-backdrop-filter:bg-card/40">
      <TooltipProvider delay={0}>
        {/* Undo/Redo */}
        <div className="flex items-center gap-0.5">
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => undo()}
                  disabled={!canUndo}
                  className={cn(
                    "h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50",
                    !canUndo && "opacity-50 cursor-not-allowed",
                  )}
                >
                  <IconPlaceholder
                    lucide="Undo"
                    hugeicons="UndoIcon"
                    tabler="IconArrowBackUp"
                    phosphor="ArrowCounterClockwise"
                    remixicon="RiArrowGoBackLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              <p className="text-xs font-medium">Undo</p>
              <p className="text-[10px] text-muted-foreground">Ctrl+Z</p>
            </TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => redo()}
                  disabled={!canRedo}
                  className={cn(
                    "h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50",
                    !canRedo && "opacity-50 cursor-not-allowed",
                  )}
                >
                  <IconPlaceholder
                    lucide="Redo"
                    hugeicons="RedoIcon"
                    tabler="IconArrowForwardUp"
                    phosphor="ArrowClockwise"
                    remixicon="RiArrowGoForwardLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              <p className="text-xs font-medium">Redo</p>
              <p className="text-[10px] text-muted-foreground">Ctrl+Shift+Z</p>
            </TooltipContent>
          </Tooltip>
        </div>

        <div className="mx-1.5 h-6 w-px bg-border/50" />

        {/* Mode Toggle */}
        <Tooltip>
          <TooltipTrigger
            render={
              <Button
                variant={isPreview ? "default" : "ghost"}
                size="icon"
                onClick={() => setMode(isPreview ? "edit" : "preview")}
                className={cn(
                  "h-8 w-8 rounded-full",
                  !isPreview &&
                    "text-muted-foreground hover:text-foreground hover:bg-muted/50",
                )}
              >
                {isPreview ? (
                  <IconPlaceholder
                    lucide="Pencil"
                    hugeicons="PencilEdit01Icon"
                    tabler="IconPencil"
                    phosphor="Pencil"
                    remixicon="RiPencilLine"
                    size={16}
                  />
                ) : (
                  <IconPlaceholder
                    lucide="Eye"
                    hugeicons="ViewIcon"
                    tabler="IconEye"
                    phosphor="Eye"
                    remixicon="RiEyeLine"
                    size={16}
                  />
                )}
              </Button>
            }
          />
          <TooltipContent side="top" sideOffset={8}>
            <p className="text-xs font-medium">
              {isPreview ? "Back to Edit Mode" : "Preview Form"}
            </p>
          </TooltipContent>
        </Tooltip>

        <div className="mx-1.5 h-6 w-px bg-border/50" />

        {/* Viewport Toggles */}
        <div className="flex items-center gap-0.5">
          {/* Desktop */}
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant={viewport === "desktop" ? "default" : "ghost"}
                  size="icon"
                  onClick={() => setViewport("desktop")}
                  className={cn(
                    "h-8 w-8 rounded-full",
                    viewport !== "desktop" &&
                      "text-muted-foreground hover:text-foreground hover:bg-muted/50",
                  )}
                >
                  <IconPlaceholder
                    lucide="Monitor"
                    hugeicons="ComputerIcon"
                    tabler="IconDeviceDesktop"
                    phosphor="Monitor"
                    remixicon="RiComputerLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              <p className="text-xs font-medium">Desktop (100%)</p>
            </TooltipContent>
          </Tooltip>

          {/* Tablet */}
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant={viewport === "tablet" ? "default" : "ghost"}
                  size="icon"
                  onClick={() => setViewport("tablet")}
                  className={cn(
                    "h-8 w-8 rounded-full",
                    viewport !== "tablet" &&
                      "text-muted-foreground hover:text-foreground hover:bg-muted/50",
                  )}
                >
                  <IconPlaceholder
                    lucide="Tablet"
                    hugeicons="Tablet01Icon"
                    tabler="IconDeviceTablet"
                    phosphor="DeviceTabletIcon"
                    remixicon="RiTabletLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              <p className="text-xs font-medium">Tablet (768px)</p>
            </TooltipContent>
          </Tooltip>

          {/* Mobile */}
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant={viewport === "mobile" ? "default" : "ghost"}
                  size="icon"
                  onClick={() => setViewport("mobile")}
                  className={cn(
                    "h-8 w-8 rounded-full",
                    viewport !== "mobile" &&
                      "text-muted-foreground hover:text-foreground hover:bg-muted/50",
                  )}
                >
                  <IconPlaceholder
                    lucide="Smartphone"
                    hugeicons="SmartPhone01Icon"
                    tabler="IconDeviceMobile"
                    phosphor="DeviceMobile"
                    remixicon="RiSmartphoneLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              <p className="text-xs font-medium">Mobile (375px)</p>
            </TooltipContent>
          </Tooltip>
        </div>

        <div className="mx-1.5 h-6 w-px bg-border/50" />

        {/* Zoom Controls */}
        <div className="flex items-center gap-0.5">
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50"
                  onClick={() => setZoom(zoom - 0.1)}
                >
                  <IconPlaceholder
                    lucide="Minus"
                    hugeicons="MinusSignIcon"
                    tabler="IconMinus"
                    phosphor="Minus"
                    remixicon="RiSubtractLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              Zoom Out
            </TooltipContent>
          </Tooltip>

          <div className="flex items-center justify-center min-w-12 px-1 text-xs font-semibold tabular-nums text-muted-foreground select-none">
            {Math.round(zoom * 100)}%
          </div>

          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50"
                  onClick={() => setZoom(zoom + 0.1)}
                >
                  <IconPlaceholder
                    lucide="Plus"
                    hugeicons="Add01Icon"
                    tabler="IconPlus"
                    phosphor="Plus"
                    remixicon="RiAddLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              Zoom In
            </TooltipContent>
          </Tooltip>

          {/* Reset Zoom */}
          <div className="mx-1 h-4 w-px bg-border/50" />
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  disabled={Math.round(zoom * 100) === 90}
                  className={cn(
                    "h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50",
                    Math.round(zoom * 100) === 90 &&
                      "opacity-50 cursor-not-allowed",
                  )}
                  onClick={() => setZoom(0.9)}
                >
                  <IconPlaceholder
                    lucide="RotateCcw"
                    hugeicons="ArrowTurnBackwardIcon"
                    tabler="IconRefresh"
                    phosphor="ArrowCounterClockwise"
                    remixicon="RiRefreshLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent side="top" sideOffset={8}>
              Reset Zoom
            </TooltipContent>
          </Tooltip>
        </div>
      </TooltipProvider>
    </div>
  );
}
registry/shadcn/builder/drag-overlay.tsx
"use client";

import {
  useBuilderStore,
  useBuilderContext,
} from "@buildnbuzz/form-builder-react";
import { FieldIcon } from "./field-icon";
import { cn } from "@/lib/utils";

/**
 * Rich drag overlay item that provides visual feedback during DnD.
 * Matches legacy DragOverlayItem but integrates with the new registry pattern.
 */
export function DragOverlayItem({
  activeId,
  activeData,
}: {
  activeId: string;
  activeData: Record<string, unknown>;
}) {
  const { registry } = useBuilderContext();
  const node = useBuilderStore((s) => s.nodes[activeId]);

  const data = activeData as Record<string, unknown>;
  const isFromSidebar = data?.from === "sidebar";
  const type = (isFromSidebar ? data?.type : node?.field?.type) as
    | string
    | undefined;
  const entry = type ? registry[type] : null;

  const label =
    entry?.sidebar?.label ?? (typeof type === "string" ? type : "Field");

  // Get field name for existing fields
  let fieldName: string | undefined;
  if (!isFromSidebar && node && "name" in node.field) {
    fieldName = node.field.name;
  }

  return (
    <div
      className={cn(
        "bg-card/95 backdrop-blur-md border-2 shadow-2xl rounded-xl p-3 min-w-[20rem] cursor-grabbing",
        "animate-in fade-in-0 zoom-in-95 duration-150",
        isFromSidebar
          ? "border-primary/50 shadow-primary/10"
          : "border-border shadow-black/10",
      )}
    >
      <div className="flex items-center gap-3">
        {/* Icon container */}
        <div
          className={cn(
            "p-2.5 rounded-lg flex items-center justify-center",
            isFromSidebar
              ? "bg-primary/15 text-primary"
              : "bg-muted text-foreground",
          )}
        >
          {type ? (
            <FieldIcon type={type} size={20} />
          ) : (
            <FieldIcon type="text" size={20} />
          )}
        </div>

        {/* Text content */}
        <div className="flex-1 min-w-0 text-left">
          <div className="text-sm font-semibold text-foreground truncate">
            {isFromSidebar ? `New ${label}` : (fieldName ?? label)}
          </div>
          <div className="text-xs text-muted-foreground flex items-center gap-1.5 whitespace-nowrap">
            <FieldIcon
              type={isFromSidebar ? "plus" : "move"}
              size={12}
            />
            <div className="flex items-center gap-1">
              {isFromSidebar ? (
                <>
                  Adding new, press
                  <kbd className="inline-flex items-center rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-foreground/80">
                    Esc
                  </kbd>
                  to cancel
                </>
              ) : (
                "Moving field"
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
registry/shadcn/builder/export-sheet.tsx
"use client";

import * as React from "react";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  generateComponentCode,
  nodesToFields,
  toSafeFileName,
} from "@buildnbuzz/form-builder-core";
import {
  useBuilderStore,
  downloadTextFile,
} from "@buildnbuzz/form-builder-react";

const starterCommand =
  "npx shadcn@latest add https://form.buildnbuzz.com/r/all.json";

export function ExportSheet() {
  const [open, setOpen] = React.useState(false);

  const nodes = useBuilderStore((state) => state.nodes);
  const rootIds = useBuilderStore((state) => state.rootIds);
  const formName = useBuilderStore((state) => state.formName);
  const outputConfig = useBuilderStore((state) => state.outputConfig);

  const componentCode = React.useMemo(() => {
    if (!open) return "";
    return generateComponentCode(nodes, rootIds, formName, outputConfig);
  }, [open, nodes, rootIds, formName, outputConfig]);

  const schemaJson = React.useMemo(() => {
    if (!open) return "";
    const fields = nodesToFields(nodes, rootIds);
    const schema = {
      title: formName || "BuzzForm Export",
      fields,
    };
    return JSON.stringify(schema, null, 2);
  }, [open, nodes, rootIds, formName]);

  const downloadSchema = React.useCallback(() => {
    if (!schemaJson) return;
    const fileName = `${toSafeFileName(formName)}.json`;
    downloadTextFile(schemaJson, fileName, "application/json");
    toast.success("BuzzForm schema exported");
  }, [schemaJson, formName]);

  const downloadCode = React.useCallback(() => {
    if (!componentCode) return;
    const fileName = `${toSafeFileName(formName)}.tsx`;
    downloadTextFile(componentCode, fileName, "text/plain;charset=utf-8");
    toast.success("Component code exported");
  }, [componentCode, formName]);

  const copyToClipboard = React.useCallback((text: string, label: string) => {
    navigator.clipboard.writeText(text);
    toast.success(`${label} copied to clipboard`);
  }, []);

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger
        render={
          <Button variant="outline" className="gap-2">
            <IconPlaceholder
              hugeicons="FileExportIcon"
              lucide="FileDown"
              tabler="IconFileExport"
              phosphor="FileArrowDown"
              remixicon="RiFileDownloadLine"
              size={16}
              strokeWidth={2}
            />
            Export
          </Button>
        }
      />

      <SheetContent
        side="right"
        className="w-full p-0 gap-0 data-[side=right]:w-[min(96vw,1120px)] data-[side=right]:sm:max-w-280"
      >
        <SheetHeader className="border-b pr-12">
          <SheetTitle>Export Form</SheetTitle>
          <SheetDescription>
            Export production-ready TSX or portable BuzzForm schema JSON.
          </SheetDescription>
        </SheetHeader>

        <Tabs
          defaultValue="tsx"
          className="flex h-full min-h-0 flex-1 flex-col p-4"
        >
          <TabsList variant="line" className="w-full">
            <TabsTrigger value="tsx" className="gap-1.5">
              <IconPlaceholder
                hugeicons="SourceCodeIcon"
                lucide="Code"
                tabler="IconCode"
                phosphor="Code"
                remixicon="RiCodeLine"
                size={16}
                strokeWidth={2}
              />
              App Code
              <Badge variant="outline">.tsx</Badge>
            </TabsTrigger>
            <TabsTrigger value="schema" className="gap-1.5">
              <IconPlaceholder
                hugeicons="File02Icon"
                lucide="File"
                tabler="IconFile"
                phosphor="File"
                remixicon="RiFileCodeLine"
                size={16}
                strokeWidth={2}
              />
              BuzzForm Schema
              <Badge variant="outline">.json</Badge>
            </TabsTrigger>
          </TabsList>

          <TabsContent value="tsx" className="min-h-0 flex-1 overflow-hidden">
            <ScrollArea className="h-full px-4 py-2">
              <div className="space-y-3 p-1">
                <div className="rounded-lg border bg-muted/30 p-3">
                  <div className="mb-3 flex items-center gap-2 text-sm font-medium">
                    <IconPlaceholder
                      hugeicons="SourceCodeIcon"
                      lucide="Code"
                      tabler="IconCode"
                      phosphor="Code"
                      remixicon="RiCodeLine"
                      size={16}
                      strokeWidth={1.8}
                      className="text-muted-foreground"
                    />
                    Required setup
                  </div>
                  <div className="[&_figure]:my-0!">
                    <DynamicCodeBlock lang="bash" code={starterCommand} />
                  </div>
                  <div className="mt-3 flex flex-wrap items-center gap-2">
                    <Button
                      type="button"
                      variant="outline"
                      size="sm"
                      onClick={downloadCode}
                    >
                      <IconPlaceholder
                        hugeicons="Download01Icon"
                        lucide="Download"
                        tabler="IconDownload"
                        phosphor="DownloadSimple"
                        remixicon="RiDownload2Line"
                        size={16}
                        strokeWidth={2}
                      />
                      Download TSX File
                    </Button>
                    <Button
                      type="button"
                      variant="ghost"
                      size="sm"
                      onClick={() => copyToClipboard(componentCode, "App code")}
                    >
                      <IconPlaceholder
                        hugeicons="Copy01Icon"
                        lucide="Copy"
                        tabler="IconCopy"
                        phosphor="Copy"
                        remixicon="RiFileCopyLine"
                        size={16}
                        strokeWidth={2}
                      />
                      Copy Code
                    </Button>
                  </div>
                </div>

                <div className="[&_figure]:my-0!">
                  <DynamicCodeBlock lang="tsx" code={componentCode} />
                </div>
              </div>
            </ScrollArea>
          </TabsContent>

          <TabsContent
            value="schema"
            className="min-h-0 flex-1 overflow-hidden"
          >
            <ScrollArea className="h-full px-4 py-2">
              <div className="space-y-3 p-1">
                <div className="rounded-lg border bg-muted/30 p-3">
                  <div className="mb-3 flex items-center gap-2 text-sm font-medium">
                    <IconPlaceholder
                      hugeicons="File02Icon"
                      lucide="File"
                      tabler="IconFile"
                      phosphor="File"
                      remixicon="RiFileCodeLine"
                      size={16}
                      strokeWidth={1.8}
                      className="text-muted-foreground"
                    />
                    BuzzForm schema
                  </div>
                  <p className="text-sm text-muted-foreground">
                    Pure schema JSON for copy/paste import and runtime form
                    generation.
                  </p>
                  <div className="mt-2 flex flex-wrap items-center gap-2">
                    <Button
                      type="button"
                      variant="outline"
                      size="sm"
                      onClick={downloadSchema}
                    >
                      <IconPlaceholder
                        hugeicons="Download01Icon"
                        lucide="Download"
                        tabler="IconDownload"
                        phosphor="DownloadSimple"
                        remixicon="RiDownload2Line"
                        size={16}
                        strokeWidth={2}
                      />
                      Download BuzzForm Schema
                    </Button>
                    <Button
                      type="button"
                      variant="ghost"
                      size="sm"
                      onClick={() => copyToClipboard(schemaJson, "Schema JSON")}
                    >
                      <IconPlaceholder
                        hugeicons="Copy01Icon"
                        lucide="Copy"
                        tabler="IconCopy"
                        phosphor="Copy"
                        remixicon="RiFileCopyLine"
                        size={16}
                        strokeWidth={2}
                      />
                      Copy JSON
                    </Button>
                  </div>
                </div>

                <div className="[&_figure]:my-0!">
                  <DynamicCodeBlock lang="json" code={schemaJson} />
                </div>
              </div>
            </ScrollArea>
          </TabsContent>
        </Tabs>
      </SheetContent>
    </Sheet>
  );
}
registry/shadcn/builder/expression-builder/expression-checkbox.tsx
"use client";

import React from "react";
import { useDataField } from "@buildnbuzz/form-react";
import type { ExpressionGroup } from "@buildnbuzz/form-builder-core";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field";
import { cn } from "@/lib/utils";
import { ExpressionBuilder } from "./index";

type ExpressionValue = boolean | undefined | ExpressionGroup;

function isExpressionGroup(value: unknown): value is ExpressionGroup {
  return (
    typeof value === "object" &&
    value !== null &&
    (value as ExpressionGroup).type === "group"
  );
}

/**
 * Dual-mode property widget for boolean/expression fields.
 *
 * - Static mode: renders a Checkbox or Switch (per `field.type`) with a ⚡ button.
 * - Expression mode: renders an "Expression active" badge with Edit and Clear buttons.
 *
 * Toggling ⚡ opens the ExpressionBuilder dialog. Saving stores an ExpressionGroup.
 * Clearing reverts to `false` (static boolean).
 */
export function ExpressionCheckbox() {
  const {
    field,
    fieldApi,
    isDisabled,
    isReadOnly,
    isRequired,
    label,
    description,
    errors,
    isInvalid,
    descriptionId,
    errorId,
    ariaDescribedBy,
    handleBlur,
    handleChange,
  } = useDataField();

  const value = fieldApi.state.value as ExpressionValue;
  const disabled = isDisabled || isReadOnly;
  const isExpression = isExpressionGroup(value);
  const boolValue = isExpression ? false : ((value as boolean | undefined) ?? false);
  const fieldType = (field as { type?: string }).type;
  
  const ui = (
    field as {
      ui?: {
        alignment?: "start" | "end" | "between";
        autoFocus?: boolean;
        className?: string;
        expressionMode?: "default" | "only" | "none";
      };
    }
  ).ui;
  const alignment = ui?.alignment ?? "between";
  const expressionMode = ui?.expressionMode ?? "default";

  const handleSave = (group: ExpressionGroup) => handleChange(group);
  const handleClear = () => handleChange(expressionMode === "only" ? undefined : false);

  // 1. The active expression node (replaces the input)
  const activeControlNode = (
    <div
      className={cn(
        "flex shrink-0 items-center justify-center border border-amber-500/50 bg-amber-500/10 text-amber-600 dark:text-amber-500",
        fieldType === "switch" ? "h-5 w-9 rounded-full" : "h-4 w-4 mt-0.5 rounded-sm"
      )}
    >
      <IconPlaceholder
        lucide="Zap"
        hugeicons="Lightning"
        tabler="IconBolt"
        phosphor="Lightning"
        remixicon="RiFlashlightLine"
        size={fieldType === "switch" ? 12 : 10}
        className="fill-amber-500/20"
      />
    </div>
  );

  const expressionBar = isExpression ? (
    <div className="mt-2 flex items-center justify-between rounded-md border border-amber-500/30 bg-amber-500/5 px-2 py-1 shadow-sm">
      <div className="flex items-center gap-1.5 text-xs font-medium text-amber-600 dark:text-amber-500">
        <IconPlaceholder
          lucide="Zap"
          hugeicons="Lightning"
          tabler="IconBolt"
          phosphor="Lightning"
          remixicon="RiFlashlightLine"
          size={14}
          className="fill-amber-500/20"
        />
        Dynamic Expression
      </div>
      <div className="flex items-center gap-0.5">
        <ExpressionBuilder
          initialValue={value as ExpressionGroup}
          onSave={handleSave}
          trigger={
            <Button
              variant="ghost"
              size="icon-sm"
              className="h-6 w-6 text-amber-600/70 hover:bg-amber-500/10 hover:text-amber-600 dark:text-amber-500/70 dark:hover:text-amber-500"
            >
              <IconPlaceholder
                lucide="Pencil"
                hugeicons="PencilEdit01Icon"
                tabler="IconPencil"
                phosphor="PencilSimple"
                remixicon="RiPencilLine"
                size={12}
              />
            </Button>
          }
        />
        <Button
          variant="ghost"
          size="icon-sm"
          className="h-6 w-6 text-amber-600/70 hover:bg-destructive/10 hover:text-destructive dark:text-amber-500/70 dark:hover:text-destructive"
          onClick={handleClear}
          disabled={disabled}
        >
          <IconPlaceholder
            lucide="X"
            hugeicons="Cancel01Icon"
            tabler="IconX"
            phosphor="X"
            remixicon="RiCloseLine"
            size={12}
          />
        </Button>
      </div>
    </div>
  ) : expressionMode === "only" && !isExpression ? (
    <div className="mt-2">
      <ExpressionBuilder
        initialValue={undefined}
        onSave={handleSave}
        trigger={
          <Button
            variant="outline"
            size="sm"
            className="h-7 w-full gap-1.5 border-dashed text-xs text-muted-foreground hover:border-amber-500/50 hover:bg-amber-500/5 hover:text-amber-500"
          >
            <IconPlaceholder
              lucide="Zap"
              hugeicons="Lightning"
              tabler="IconBolt"
              phosphor="Lightning"
              remixicon="RiFlashlightLine"
              size={14}
            />
            Set Expression
          </Button>
        }
      />
    </div>
  ) : null;

  // 2. The static control node
  const switchNode = (
    <Switch
      id={fieldApi.name}
      checked={boolValue}
      onCheckedChange={(checked) => {
        handleChange(checked);
        handleBlur();
      }}
      disabled={disabled}
      aria-invalid={isInvalid}
      aria-describedby={ariaDescribedBy}
      aria-readonly={isReadOnly}
      autoFocus={ui?.autoFocus}
      size="sm"
    />
  );

  const checkboxNode = (
    <Checkbox
      id={fieldApi.name}
      checked={boolValue}
      onCheckedChange={(checked) => {
        handleChange(!!checked);
        handleBlur();
      }}
      disabled={disabled}
      aria-invalid={isInvalid}
      aria-describedby={ariaDescribedBy}
      aria-readonly={isReadOnly}
      autoFocus={ui?.autoFocus}
    />
  );

  const emptyControlNode = (
    <div
      className={cn(
        "flex shrink-0 items-center justify-center text-muted-foreground/30",
        fieldType === "switch" ? "h-5 w-9" : "h-4 w-4 mt-0.5"
      )}
    >
      <IconPlaceholder
        lucide="Zap"
        hugeicons="Lightning"
        tabler="IconBolt"
        phosphor="Lightning"
        remixicon="RiFlashlightLine"
        size={fieldType === "switch" ? 12 : 10}
      />
    </div>
  );

  const controlNode = isExpression
    ? activeControlNode
    : expressionMode === "only"
      ? emptyControlNode
      : fieldType === "switch"
        ? switchNode
        : checkboxNode;

  // 3. The content element (label, description, error, expression bar)
  const isSwitchBetween = fieldType === "switch" && alignment === "between";
  const contentElement = (
    <FieldContent className={isSwitchBetween ? "flex-1" : undefined}>
      {label && (
        <FieldLabel
          htmlFor={fieldApi.name}
          className={cn(
            "cursor-pointer m-0 font-normal",
            isSwitchBetween ? "flex-none gap-1 items-baseline" : "inline"
          )}
        >
          {isSwitchBetween && isRequired && <span className="text-destructive">*</span>}
          {label}
          {!isSwitchBetween && isRequired && <span className="text-destructive">&nbsp;*</span>}
        </FieldLabel>
      )}
      {description && !isInvalid && (
        <FieldDescription id={descriptionId}>{description}</FieldDescription>
      )}
      {isInvalid && <FieldError id={errorId} errors={errors} />}
      {expressionBar}
    </FieldContent>
  );

  // 4. Assemble the layout
  return (
    <FieldGroup data-field={fieldApi.name} className={cn("group/expr", ui?.className)}>
      <div className="flex items-start gap-2 w-full">
        <Field
          orientation="horizontal"
          className={cn("flex-1", isSwitchBetween && "w-full justify-between")}
          data-invalid={isInvalid}
          data-disabled={disabled}
        >
          {fieldType === "switch" ? (
            alignment === "start" ? (
              <>
                {controlNode}
                {contentElement}
              </>
            ) : (
              <>
                {contentElement}
                {controlNode}
              </>
            )
          ) : (
            // Default Checkbox layout
            <>
              {controlNode}
              {contentElement}
            </>
          )}
        </Field>

        {/* Lightning bolt trigger (only visible when static mode and default mode) */}
        {!isExpression && expressionMode === "default" && (
          <ExpressionBuilder
            initialValue={undefined}
            onSave={handleSave}
            trigger={
              <Button
                variant="ghost"
                size="icon-sm"
                className="h-6 w-6 shrink-0 opacity-0 transition-all hover:bg-amber-500/10 hover:text-amber-500 focus-visible:opacity-100 group-hover/expr:opacity-100 dark:text-muted-foreground"
                disabled={disabled}
              >
                <IconPlaceholder
                  lucide="Zap"
                  hugeicons="Lightning"
                  tabler="IconBolt"
                  phosphor="Lightning"
                  remixicon="RiFlashlightLine"
                  size={14}
                />
              </Button>
            }
          />
        )}
      </div>
    </FieldGroup>
  );
}
registry/shadcn/builder/expression-builder/expression-group.tsx
"use client";

import React from "react";
import type { StoreApi } from "zustand";
import { cn } from "@/lib/utils";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
  Empty,
  EmptyHeader,
  EmptyTitle,
  EmptyDescription,
  EmptyMedia,
} from "@/components/ui/empty";
import {
  SortableContext,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { type ExpressionGroup } from "@buildnbuzz/form-builder-core";
import { type ExpressionStoreState } from "@buildnbuzz/form-builder-react";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { SortableItem } from "./sortable-item";
import { ExpressionRuleItem } from "./expression-rule";

interface ExpressionGroupItemProps {
  group: ExpressionGroup;
  parentId?: string;
  isRoot?: boolean;
  store: StoreApi<ExpressionStoreState>;
}

export function ExpressionGroupItem({
  group,
  parentId,
  isRoot = false,
  store,
}: ExpressionGroupItemProps) {
  const [isOpen, setIsOpen] = React.useState(true);
  const {
    updateGroupOperator,
    removeNode,
    addRule,
    addGroup,
    duplicateGroup,
  } = store.getState();

  const headerContent = (
    <div
      className={cn(
        "flex flex-col sm:flex-row items-start sm:items-center gap-2 p-1.5 transition-all relative",
        isRoot
          ? "rounded-xl"
          : cn(
              "bg-muted/50 cursor-pointer group/header hover:bg-muted/80 rounded-t-xl",
              isOpen ? "rounded-b-none" : "rounded-b-xl"
            )
      )}
    >
      {!isRoot && (
        <div className="flex items-center justify-center w-6 h-6 shrink-0 text-muted-foreground mr-1">
          {isOpen ? (
            <IconPlaceholder
              lucide="ChevronDown"
              hugeicons="ArrowDown01Icon"
              tabler="IconChevronDown"
              phosphor="CaretDown"
              remixicon="RiArrowDownSLine"
              size={18}
            />
          ) : (
            <IconPlaceholder
              lucide="ChevronRight"
              hugeicons="ArrowRight01Icon"
              tabler="IconChevronRight"
              phosphor="CaretRight"
              remixicon="RiArrowRightSLine"
              size={18}
            />
          )}
        </div>
      )}

      <div onClick={(e) => e.stopPropagation()}>
        <Select
          value={group.logicalOperator}
          onValueChange={(val: unknown) => {
            if (val) updateGroupOperator(group.id, val as "AND" | "OR");
          }}
        >
          <SelectTrigger className="w-26.25 text-xs font-medium bg-muted border-none">
            <SelectValue>
              {group.logicalOperator === "AND" ? "Match ALL" : "Match ANY"}
            </SelectValue>
          </SelectTrigger>
          <SelectContent
            alignItemWithTrigger={false}
            align="start"
            sideOffset={4}
          >
            <SelectItem value="AND">Match ALL</SelectItem>
            <SelectItem value="OR">Match ANY</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <span className="text-sm font-medium text-muted-foreground">
        {isRoot ? "of the following:" : "Expression Group"}
      </span>

      {!isRoot && parentId && (
        <div
          className="absolute -top-3 right-4 flex items-center bg-card border shadow-sm rounded-md px-1 py-0.5 opacity-0 group-hover/header:opacity-100 transition-all z-30 scale-95 group-hover/header:scale-100"
          onClick={(e) => e.stopPropagation()}
        >
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon-lg"
                  className="h-7 w-7 text-muted-foreground hover:text-primary"
                  onClick={(e) => {
                    e.stopPropagation();
                    duplicateGroup(parentId, group.id);
                  }}
                >
                  <IconPlaceholder
                    lucide="Copy"
                    hugeicons="Copy01Icon"
                    tabler="IconCopy"
                    phosphor="Copy"
                    remixicon="RiFileCopyLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent>Duplicate Expression Group</TooltipContent>
          </Tooltip>

          <div className="w-px h-4 bg-border mx-0.5" />

          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-7 w-7 text-muted-foreground hover:text-destructive"
                  onClick={(e) => {
                    e.stopPropagation();
                    removeNode(parentId, group.id);
                  }}
                >
                  <IconPlaceholder
                    lucide="Trash2"
                    hugeicons="Delete01Icon"
                    tabler="IconTrash"
                    phosphor="Trash"
                    remixicon="RiDeleteBinLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent>Delete Expression Group</TooltipContent>
          </Tooltip>
        </div>
      )}
    </div>
  );

  const mainContent =
    group.children.length === 0 ? (
      <Empty className={isRoot ? "h-full min-h-75" : "h-auto py-8"}>
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconPlaceholder
              lucide="Layers"
              hugeicons="Layers01Icon"
              tabler="IconLayersIntersect"
              phosphor="Stack"
              remixicon="RiStackLine"
            />
          </EmptyMedia>
          <EmptyTitle>No rules added</EmptyTitle>
          <EmptyDescription>
            Click the buttons below to add rules
          </EmptyDescription>
        </EmptyHeader>
      </Empty>
    ) : (
      <SortableContext
        id={group.id}
        items={group.children.map((c) => c.id)}
        strategy={verticalListSortingStrategy}
      >
        <div className="flex flex-col gap-3">
          {group.children.map((child) => (
            <SortableItem key={child.id} id={child.id}>
              {child.type === "group" ? (
                <ExpressionGroupItem
                  group={child}
                  parentId={group.id}
                  store={store}
                />
              ) : (
                <ExpressionRuleItem
                  rule={child}
                  parentId={group.id}
                  store={store}
                />
              )}
            </SortableItem>
          ))}
        </div>
      </SortableContext>
    );

  const footerActions = (
    <div className="flex items-center gap-2">
      <Button
        variant="outline"
        size="sm"
        className="h-8 text-xs"
        onClick={() => addRule(group.id)}
      >
        <span className="mr-1 flex items-center justify-center">
          <IconPlaceholder
            lucide="Plus"
            hugeicons="PlusSignIcon"
            tabler="IconPlus"
            phosphor="Plus"
            remixicon="RiAddLine"
            size={14}
          />
        </span>
        Add Rule
      </Button>
      <Button
        variant="outline"
        size="sm"
        className="h-8 text-xs"
        onClick={() => addGroup(group.id)}
      >
        <span className="mr-1 flex items-center justify-center">
          <IconPlaceholder
            lucide="FolderPlus"
            hugeicons="FolderAddIcon"
            tabler="IconFolderPlus"
            phosphor="FolderPlus"
            remixicon="RiFolderAddLine"
            size={14}
          />
        </span>
        Add Group
      </Button>
    </div>
  );

  if (isRoot) {
    return (
      <div className="flex flex-col flex-1 min-h-0 h-full">
        <div className="shrink-0 px-6 pt-0 pb-0">{headerContent}</div>
        <ScrollArea className="flex-1 h-full min-h-0">
          <div className="px-6 pt-4 pb-2">{mainContent}</div>
        </ScrollArea>
        <div className="shrink-0 px-6 pt-2 pb-0">{footerActions}</div>
      </div>
    );
  }

  return (
    <div className="overflow-visible w-full">
      <Card
        size="sm"
        className="gap-0 py-0 data-[size=sm]:py-0 ring-1 ring-border shadow-sm border-none bg-card overflow-visible"
      >
        <Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
          <CollapsibleTrigger
            nativeButton={false}
            render={headerContent}
            className="w-full"
          />
          <CollapsibleContent>
            <CardContent className="pt-2 pb-0">{mainContent}</CardContent>
            <CardFooter className="bg-transparent border-none px-4 py-2">
              {footerActions}
            </CardFooter>
          </CollapsibleContent>
        </Collapsible>
      </Card>
    </div>
  );
}
registry/shadcn/builder/expression-builder/expression-rule.tsx
"use client";

import type { StoreApi } from "zustand";
import {
  type ExpressionRule,
  type ExpressionOperator,
  EXPRESSION_OPERATORS,
  getFieldTypeCategory,
  getOperatorsForFieldType,
} from "@buildnbuzz/form-builder-core";
import {
  useAvailableFields,
  type ExpressionStoreState,
} from "@buildnbuzz/form-builder-react";

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { IconPlaceholder } from "@/components/icon-placeholder";

interface ExpressionRuleItemProps {
  rule: ExpressionRule;
  parentId: string;
  store: StoreApi<ExpressionStoreState>;
}

export function ExpressionRuleItem({
  rule,
  parentId,
  store,
}: ExpressionRuleItemProps) {
  const availableFields = useAvailableFields();

  const selectedField = availableFields.find((f) => f.id === rule.fieldId);
  const fieldCategory = getFieldTypeCategory(selectedField?.type);
  const applicableOperators = getOperatorsForFieldType(selectedField?.type);

  const selectedOperator = EXPRESSION_OPERATORS.find(
    (op) => op.value === rule.operator,
  );
  const requiresValue = selectedOperator?.requiresValue ?? true;

  const selectedValueField = availableFields.find((f) => f.id === rule.value);

  const { updateRule, removeNode, duplicateRule } = store.getState();

  const handleFieldChange = (newFieldId: string) => {
    const newField = availableFields.find((f) => f.id === newFieldId);
    const newCategory = getFieldTypeCategory(newField?.type);
    const newOps = getOperatorsForFieldType(newField?.type);

    // Reset operator if not applicable to new field type
    const currentOpApplicable = newOps.some((op) => op.value === rule.operator);
    const newOperator = currentOpApplicable ? rule.operator : "equals";

    // Reset value for type changes
    let newValue = rule.value;
    if (newCategory === "boolean") {
      newValue = "true";
    } else if (newCategory !== fieldCategory) {
      newValue = "";
    }

    updateRule(parentId, rule.id, {
      fieldId: newFieldId,
      operator: newOperator,
      value: newValue,
      valueType: newCategory === "boolean" ? "custom" : rule.valueType,
    });
  };

  // Render value input based on field type category
  const renderValueInput = () => {
    if (!requiresValue) return null;

    // Field reference mode
    if (rule.valueType !== "custom") {
      return (
        <div className="flex items-center gap-2 w-full sm:flex-1">
          <Tooltip>
            <TooltipTrigger
              render={
                <Button
                  variant="outline"
                  size="icon-lg"
                  className="shrink-0 text-muted-foreground"
                  onClick={() =>
                    updateRule(parentId, rule.id, {
                      valueType: "custom",
                      value: selectedField?.options?.[0]?.value ?? "",
                    })
                  }
                >
                  <IconPlaceholder
                    lucide="Link"
                    hugeicons="LinkSquare02Icon"
                    tabler="IconLink"
                    phosphor="Link"
                    remixicon="RiLinksLine"
                    size={16}
                  />
                </Button>
              }
            />
            <TooltipContent>Use Custom Value</TooltipContent>
          </Tooltip>
          <Select
            value={rule.value}
            onValueChange={(val: unknown) => {
              if (val)
                updateRule(parentId, rule.id, {
                  value: val as string,
                });
            }}
          >
            <SelectTrigger className="w-full h-9!">
              <SelectValue>{selectedValueField?.label}</SelectValue>
            </SelectTrigger>
            <SelectContent
              alignItemWithTrigger={false}
              align="start"
              sideOffset={4}
            >
              {availableFields
                .filter((f) => f.id !== rule.fieldId)
                .map((field) => (
                  <SelectItem key={field.id} value={field.id}>
                    {field.label}
                  </SelectItem>
                ))}
            </SelectContent>
          </Select>
        </div>
      );
    }

    const fieldRefButton = (
      <Tooltip>
        <TooltipTrigger
          render={
            <Button
              variant="outline"
              size="icon-lg"
              className="shrink-0 text-muted-foreground"
              onClick={() =>
                updateRule(parentId, rule.id, {
                  valueType: "field",
                  value: "",
                })
              }
            >
              <IconPlaceholder
                lucide="Type"
                hugeicons="TextIcon"
                tabler="IconLetterCase"
                phosphor="TextT"
                remixicon="RiText"
                size={16}
              />
            </Button>
          }
        />
        <TooltipContent>Use Field Reference</TooltipContent>
      </Tooltip>
    );

    // Fields with options (boolean, select, radio, checkbox-group): render Select dropdown
    if (selectedField?.options && selectedField.options.length > 0) {
      const matchedOption = selectedField.options.find(
        (o) => o.value === rule.value,
      );
      return (
        <div className="flex items-center gap-2 w-full sm:flex-1">
          {fieldRefButton}
          <Select
            value={rule.value}
            onValueChange={(val: unknown) => {
              if (val) updateRule(parentId, rule.id, { value: val as string });
            }}
          >
            <SelectTrigger className="w-full h-9!">
              <SelectValue>{matchedOption?.label ?? rule.value}</SelectValue>
            </SelectTrigger>
            <SelectContent
              alignItemWithTrigger={false}
              align="start"
              sideOffset={4}
            >
              {selectedField.options.map((opt) => (
                <SelectItem key={opt.value} value={opt.value}>
                  {opt.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      );
    }

    // Number fields: render number input
    if (fieldCategory === "number") {
      return (
        <div className="flex items-center gap-2 w-full sm:flex-1">
          {fieldRefButton}
          <Input
            type="number"
            placeholder="Enter number..."
            className="h-9 w-full"
            value={rule.value}
            onChange={(e) =>
              updateRule(parentId, rule.id, { value: e.target.value })
            }
          />
        </div>
      );
    }

    // Default: text input
    return (
      <div className="flex items-center gap-2 w-full sm:flex-1">
        {fieldRefButton}
        <Input
          placeholder="Enter value..."
          className="h-9 w-full"
          value={rule.value}
          onChange={(e) =>
            updateRule(parentId, rule.id, { value: e.target.value })
          }
        />
      </div>
    );
  };

  return (
    <div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 bg-card p-2 rounded-lg border shadow-sm transition-all group/rule w-full relative">
      <Select
        value={rule.fieldId}
        onValueChange={(val: unknown) => {
          if (val) handleFieldChange(val as string);
        }}
      >
        <SelectTrigger className="w-full sm:w-50 h-9!">
          <SelectValue>{selectedField?.label}</SelectValue>
        </SelectTrigger>
        <SelectContent
          alignItemWithTrigger={false}
          align="start"
          sideOffset={4}
        >
          {availableFields.map((field) => (
            <SelectItem key={field.id} value={field.id}>
              {field.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>

      <Select
        value={rule.operator}
        onValueChange={(val: unknown) => {
          if (val)
            updateRule(parentId, rule.id, {
              operator: val as ExpressionOperator,
            });
        }}
      >
        <SelectTrigger className="w-full sm:w-40 h-9!">
          <SelectValue>{selectedOperator?.label}</SelectValue>
        </SelectTrigger>
        <SelectContent
          alignItemWithTrigger={false}
          align="start"
          sideOffset={4}
        >
          {applicableOperators.map((op) => (
            <SelectItem key={op.value} value={op.value}>
              {op.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>

      {renderValueInput()}

      <div className="absolute -top-3 right-4 flex items-center bg-card border shadow-sm rounded-md px-1 py-0.5 opacity-0 group-hover/rule:opacity-100 transition-all z-50 scale-95 group-hover/rule:scale-100">
        <Tooltip>
          <TooltipTrigger
            render={
              <Button
                variant="ghost"
                size="icon-lg"
                className="text-muted-foreground hover:text-primary h-7 w-7"
                onClick={() => duplicateRule(parentId, rule.id)}
              >
                <IconPlaceholder
                  lucide="Copy"
                  hugeicons="Copy01Icon"
                  tabler="IconCopy"
                  phosphor="Copy"
                  remixicon="RiFileCopyLine"
                  size={16}
                />
              </Button>
            }
          />
          <TooltipContent>Duplicate Rule</TooltipContent>
        </Tooltip>

        <div className="w-px h-4 bg-border mx-0.5" />

        <Tooltip>
          <TooltipTrigger
            render={
              <Button
                variant="ghost"
                size="icon"
                className="text-muted-foreground hover:text-destructive h-7 w-7"
                onClick={() => removeNode(parentId, rule.id)}
              >
                <IconPlaceholder
                  lucide="Trash2"
                  hugeicons="Delete01Icon"
                  tabler="IconTrash"
                  phosphor="Trash"
                  remixicon="RiDeleteBinLine"
                  size={16}
                />
              </Button>
            }
          />
          <TooltipContent>Delete Rule</TooltipContent>
        </Tooltip>
      </div>
    </div>
  );
}
registry/shadcn/builder/expression-builder/index.tsx
"use client";

import React, { useState } from "react";
import { DndContext, closestCorners } from "@dnd-kit/core";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { IconPlaceholder } from "@/components/icon-placeholder";

import type { ExpressionGroup } from "@buildnbuzz/form-builder-core";
import {
  useExpressionStore,
  useExpressionDnd,
} from "@buildnbuzz/form-builder-react";

import { ExpressionGroupItem } from "./expression-group";

export { ExpressionGroupItem } from "./expression-group";
export { ExpressionRuleItem } from "./expression-rule";
export { SortableItem } from "./sortable-item";

export interface ExpressionBuilderProps {
  initialValue?: ExpressionGroup;
  onSave: (group: ExpressionGroup) => void;
  trigger?: React.ReactElement;
}

export function ExpressionBuilder({
  initialValue,
  onSave,
  trigger,
}: ExpressionBuilderProps) {
  const [open, setOpen] = useState(false);

  const { store, rootGroup } = useExpressionStore(initialValue, open);
  const { sensors, handleDragOver, handleDragEnd } = useExpressionDnd(store);

  const handleSave = () => {
    onSave(store.getState().rootGroup);
    setOpen(false);
  };

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger
        render={
          trigger || (
            <Button variant="outline">
              <span className="mr-2 flex items-center justify-center">
                <IconPlaceholder
                  lucide="Settings"
                  hugeicons="Settings01Icon"
                  tabler="IconSettings"
                  phosphor="Gear"
                  remixicon="RiSettings4Line"
                  size={16}
                />
              </span>
              Expression Logic
            </Button>
          )
        }
      />
      <DialogContent className="sm:max-w-4xl md:max-w-5xl lg:max-w-6xl w-[95vw] h-[80vh] flex flex-col p-0 overflow-hidden bg-background/80">
        <DialogHeader className="p-6 pb-4 bg-card border-b shrink-0 flex flex-row items-start gap-4 space-y-0 text-left">
          <div className="text-primary flex items-center justify-center p-2 rounded-xl bg-primary/10">
            <IconPlaceholder
              lucide="Settings"
              hugeicons="Settings01Icon"
              tabler="IconSettings"
              phosphor="Gear"
              remixicon="RiSettings4Line"
              size={24}
            />
          </div>
          <div className="flex flex-col gap-1">
            <DialogTitle>Expression Builder</DialogTitle>
            <DialogDescription>
              Define the logical expression for this property.
            </DialogDescription>
          </div>
        </DialogHeader>

        <div className="flex-1 flex flex-col min-h-0">
          <DndContext
            sensors={sensors}
            collisionDetection={closestCorners}
            onDragOver={handleDragOver}
            onDragEnd={handleDragEnd}
          >
            <ExpressionGroupItem
              group={rootGroup}
              isRoot={true}
              store={store}
            />
          </DndContext>
        </div>

        <DialogFooter className="border-t shrink-0 m-0 rounded-none">
          <Button variant="outline" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            onClick={handleSave}
            className="bg-primary hover:bg-primary/90 text-primary-foreground"
          >
            Save Expression
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
registry/shadcn/builder/expression-builder/sortable-item.tsx
"use client";

import React from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { IconPlaceholder } from "@/components/icon-placeholder";

interface SortableItemProps {
  id: string;
  children: React.ReactNode;
}

export function SortableItem({ id, children }: SortableItemProps) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className="flex items-start gap-2 relative group/item"
    >
      <div
        {...attributes}
        {...listeners}
        className="mt-2 cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground opacity-0 group-hover/item:opacity-100 transition-opacity"
      >
        <IconPlaceholder
          lucide="GripVertical"
          hugeicons="DragDropVerticalIcon"
          tabler="IconGripVertical"
          phosphor="DotsSixVertical"
          remixicon="RiDragMove2Line"
          size={20}
        />
      </div>
      <div className="flex-1 min-w-0">{children}</div>
    </div>
  );
}
registry/shadcn/builder/field-icon.tsx
"use client";

import React from "react";
import { IconPlaceholder } from "@/components/icon-placeholder";

interface FieldIconProps extends React.ComponentProps<"svg"> {
  type: string;
  size?: number;
}

/**
 * A statically-analyzable icon picker for the form builder.
 *
 * IMPORTANT: We use a switch statement with literal string props for IconPlaceholder
 * so that the shadcn CLI transformer can identify and replace these with the
 * user's local icon library components during installation.
 */
export const FieldIcon = ({ type, size = 16, ...props }: FieldIconProps) => {
  switch (type) {
    case "text":
      return (
        <IconPlaceholder
          lucide="Type"
          hugeicons="TextIcon"
          tabler="IconLetterCase"
          phosphor="TextT"
          remixicon="RiText"
          size={size}
          {...props}
        />
      );
    case "email":
      return (
        <IconPlaceholder
          lucide="Mail"
          hugeicons="Mail01Icon"
          tabler="IconMail"
          phosphor="Envelope"
          remixicon="RiMailLine"
          size={size}
          {...props}
        />
      );
    case "password":
      return (
        <IconPlaceholder
          lucide="Lock"
          hugeicons="SecurityPasswordIcon"
          tabler="IconLock"
          phosphor="LockSimple"
          remixicon="RiLockPasswordLine"
          size={size}
          {...props}
        />
      );
    case "textarea":
      return (
        <IconPlaceholder
          lucide="AlignLeft"
          hugeicons="TextAlignLeftIcon"
          tabler="IconAlignLeft"
          phosphor="TextAlignLeft"
          remixicon="RiAlignLeft"
          size={size}
          {...props}
        />
      );
    case "number":
      return (
        <IconPlaceholder
          lucide="Binary"
          hugeicons="GridIcon"
          tabler="IconHash"
          phosphor="Hash"
          remixicon="RiNumber7"
          size={size}
          {...props}
        />
      );
    case "checkbox":
      return (
        <IconPlaceholder
          lucide="CheckSquare"
          hugeicons="CheckmarkSquare02Icon"
          tabler="IconCheck"
          phosphor="Check"
          remixicon="RiCheckLine"
          size={size}
          {...props}
        />
      );
    case "switch":
      return (
        <IconPlaceholder
          lucide="ToggleRight"
          hugeicons="ToggleOnIcon"
          tabler="IconToggleRight"
          phosphor="ToggleRight"
          remixicon="RiToggleLine"
          size={size}
          {...props}
        />
      );
    case "select":
      return (
        <IconPlaceholder
          lucide="ChevronDown"
          hugeicons="ArrowDown01Icon"
          tabler="IconChevronDown"
          phosphor="CaretDown"
          remixicon="RiArrowDownSLine"
          size={size}
          {...props}
        />
      );
    case "radio":
      return (
        <IconPlaceholder
          lucide="CircleDot"
          hugeicons="CircleIcon"
          tabler="IconCircleDot"
          phosphor="CircleDashed"
          remixicon="RiRadioButtonLine"
          size={size}
          {...props}
        />
      );
    case "date":
      return (
        <IconPlaceholder
          lucide="Calendar"
          hugeicons="Calendar03Icon"
          tabler="IconCalendar"
          phosphor="Calendar"
          remixicon="RiCalendarLine"
          size={size}
          {...props}
        />
      );
    case "tags":
      return (
        <IconPlaceholder
          lucide="Tag"
          hugeicons="Tag01Icon"
          tabler="IconTag"
          phosphor="Tag"
          remixicon="RiPriceTagLine"
          size={size}
          {...props}
        />
      );
    case "group":
      return (
        <IconPlaceholder
          lucide="Folder"
          hugeicons="FolderIcon"
          tabler="IconFolder"
          phosphor="Folder"
          remixicon="RiFolderLine"
          size={size}
          {...props}
        />
      );
    case "array":
      return (
        <IconPlaceholder
          lucide="Layers"
          hugeicons="Menu01Icon"
          tabler="IconLayersIntersect"
          phosphor="Stack"
          remixicon="RiStackLine"
          size={size}
          {...props}
        />
      );
    case "row":
      return (
        <IconPlaceholder
          lucide="Columns"
          hugeicons="RowInsertIcon"
          tabler="IconColumns"
          phosphor="Columns"
          remixicon="RiLayoutColumnLine"
          size={size}
          {...props}
        />
      );
    case "tabs":
      return (
        <IconPlaceholder
          lucide="PanelTop"
          hugeicons="Layout01Icon"
          tabler="IconBrowser"
          phosphor="Browser"
          remixicon="RiWindowLine"
          size={size}
          {...props}
        />
      );
    case "collapsible":
      return (
        <IconPlaceholder
          lucide="ChevronsDownUp"
          hugeicons="ArrowShrink02Icon"
          tabler="IconChevronDownLeft"
          phosphor="CaretDoubleDown"
          remixicon="RiArrowDownDoubleLine"
          size={size}
          {...props}
        />
      );
    case "upload":
      return (
        <IconPlaceholder
          lucide="UploadCloud"
          hugeicons="Upload01Icon"
          tabler="IconUpload"
          phosphor="UploadSimple"
          remixicon="RiUploadCloud2Line"
          size={size}
          {...props}
        />
      );


    // Contextual icons
    case "plus":
      return (
        <IconPlaceholder
          lucide="Plus"
          hugeicons="Add01Icon"
          tabler="IconPlus"
          phosphor="Plus"
          remixicon="RiAddLine"
          size={size}
          {...props}
        />
      );
    case "move":
      return (
        <IconPlaceholder
          lucide="Move"
          hugeicons="Move01Icon"
          tabler="IconArrowsMove"
          phosphor="ArrowsOutCardinal"
          remixicon="RiDragMove2Line"
          size={size}
          {...props}
        />
      );

    default:
      return (
        <IconPlaceholder
          lucide="Circle"
          hugeicons="CircleIcon"
          tabler="IconCircle"
          phosphor="Circle"
          remixicon="RiCircleLine"
          size={size}
          {...props}
        />
      );
  }
};
registry/shadcn/builder/field-sidebar.tsx
"use client";

import React from "react";
import {
  BuilderSidebar,
  DraggableSidebarItem,
} from "@buildnbuzz/form-builder-react";
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
} from "@/components/ui/sidebar";
import { FieldIcon } from "./field-icon";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

/**
 * Visual field palette for the form builder.
 * Grouped by category and draggable into the canvas.
 */
export const FieldSidebar = () => {
  return (
    <Sidebar className="h-full bg-background" collapsible="none">
      <SidebarContent>
        <BuilderSidebar
          render={({ groups }) => (
            <>
              {Object.entries(groups).map(([category, items]) => (
                <SidebarGroup key={category}>
                  <SidebarGroupLabel>
                    {category}
                  </SidebarGroupLabel>
                  <SidebarGroupContent>
                    <SidebarMenu>
                      {items.map((item) => (
                        <DraggableSidebarItem
                          key={item.type}
                          type={item.type}
                          render={({
                            setNodeRef,
                            attributes,
                            listeners,
                            isDragging,
                          }) => {
                            const disabled = item.disabled;

                            return (
                              <SidebarMenuItem key={item.type}>
                                <SidebarMenuButton
                                  ref={setNodeRef}
                                  {...listeners}
                                  {...attributes}
                                  className={cn(
                                    "cursor-grab group relative",
                                    disabled && "cursor-not-allowed opacity-50",
                                    isDragging && "opacity-40 bg-muted/50",
                                  )}
                                  // disabled={disabled} // DraggableSidebarItem handles disabled state
                                >
                                  <FieldIcon type={item.type} size={16} />
                                  <span>{item.label}</span>
                                  {disabled && (
                                    <Badge
                                      variant="outline"
                                      className="ml-auto text-[10px] h-5 px-1.5 py-0 bg-transparent text-muted-foreground border-muted-foreground/40"
                                    >
                                      Coming Soon
                                    </Badge>
                                  )}
                                </SidebarMenuButton>
                              </SidebarMenuItem>
                            );
                          }}
                        />
                      ))}
                    </SidebarMenu>
                  </SidebarGroupContent>
                </SidebarGroup>
              ))}
            </>
          )}
        />
      </SidebarContent>
    </Sidebar>
  );
};
registry/shadcn/builder/form-builder.tsx
"use client";

import React, { useEffect, useRef, useState } from "react";
import {
  BuilderDndProvider,
  BuilderCanvas as HeadlessCanvas,
  useBuilderStore,
  DefaultBuilderProvider,
  BuilderFormProvider,
  getBrowserLocalStorageProvider,
} from "@buildnbuzz/form-builder-react";
import { type BuilderStorageProvider } from "@buildnbuzz/form-builder-core";
import { SHADCN_BUILDER_REGISTRY } from "./registry";
import { SiteHeader } from "./header";

import { FieldSidebar } from "./field-sidebar";
import { NodeWrapper } from "./node-wrapper";
import { PropertyPanel } from "./property-panel";
import { DragOverlayItem } from "./drag-overlay";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { useDroppable } from "@dnd-kit/core";
import {
  SortableContext,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { cn } from "@/lib/utils";
import { SidebarProvider } from "@/components/ui/sidebar";
import { CanvasToolbar } from "./canvas/canvas-toolbar";
import { MobileOverlay } from "./mobile-overlay";
import { toast } from "sonner";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";

// Inline SubmitToastContent
function SubmitToastContent({ data }: { data: Record<string, unknown> }) {
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    navigator.clipboard.writeText(JSON.stringify(data, null, 2));
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div className="relative mt-2 w-full">
      <Button
        variant="ghost"
        size="icon"
        onClick={handleCopy}
        className="absolute right-2 top-2 h-7 w-7"
        title="Copy JSON"
      >
        {copied ? (
          <IconPlaceholder
            lucide="Check"
            hugeicons="Tick01Icon"
            tabler="IconCheck"
            phosphor="Check"
            remixicon="RiCheckLine"
            size={14}
            className="text-primary"
          />
        ) : (
          <IconPlaceholder
            lucide="Copy"
            hugeicons="Copy01Icon"
            tabler="IconCopy"
            phosphor="Copy"
            remixicon="RiFileCopyLine"
            size={14}
          />
        )}
      </Button>
      <pre className="max-h-75 overflow-auto rounded-md bg-muted p-3 pt-6 text-xs text-muted-foreground sm:p-3 sm:pr-10 sm:pt-3">
        <code>{JSON.stringify(data, null, 2)}</code>
      </pre>
    </div>
  );
}

const EmptyCanvas = () => {
  return (
    <Empty className="relative z-10 h-full border-0 bg-transparent min-h-60 pointer-events-none select-none">
      <EmptyMedia>
        <IconPlaceholder
          lucide="MousePointerClick"
          hugeicons="DragDropIcon"
          tabler="IconHandClick"
          phosphor="HandArrowUp"
          remixicon="RiDragDropLine"
          size={24}
        />
      </EmptyMedia>
      <EmptyContent className="max-w-[70%]">
        <EmptyTitle className="text-sm font-medium">Empty Canvas</EmptyTitle>
        <EmptyDescription className="text-xs">
          Drag components from the left sidebar to start building your form.
        </EmptyDescription>
      </EmptyContent>
    </Empty>
  );
};

interface WindowFrameProps {
  children: React.ReactNode;
  className?: string;
}

export function WindowFrame({ children, className }: WindowFrameProps) {
  const viewport = useBuilderStore((state) => state.viewport);
  const zoom = useBuilderStore((state) => state.zoom);
  const mode = useBuilderStore((state) => state.mode);

  const getViewportWidth = () => {
    switch (viewport) {
      case "mobile":
        return "375px";
      case "tablet":
        return "768px";
      default:
        return "100%";
    }
  };

  return (
    <div
      className={cn(
        "bg-background h-160 border border-border rounded-xl shadow-[0_0_60px_-5px_hsl(var(--primary)/0.4)] flex flex-col relative transition-all duration-300 ease-in-out origin-top overflow-hidden",
        className,
      )}
      style={{
        transform: `scale(${zoom})`,
        width: getViewportWidth(),
        maxWidth: viewport === "desktop" ? "896px" : getViewportWidth(),
      }}
    >
      <div className="h-10 bg-muted/50 border-b flex items-center px-4 justify-between shrink-0">
        <div className="flex items-center gap-1.5 w-20">
          <div className="w-3 h-3 rounded-full bg-destructive/80" />
          <div className="w-3 h-3 rounded-full bg-yellow-500/80" />
          <div className="w-3 h-3 rounded-full bg-green-500/80" />
        </div>
        <div className="text-xs text-muted-foreground font-medium flex-1 text-center">
          {mode === "preview" ? "Preview Mode" : "Edit Mode"}
        </div>
        <div className="w-20" />
      </div>

      {/* Content Area */}
      <ScrollArea className="flex-1 overflow-hidden">{children}</ScrollArea>
    </div>
  );
}

import { PreviewForm } from "./preview/preview-form";

/**
 * Centered frame for the canvas to simulate different viewports.
 */
const CanvasFrame = () => {
  const rootIds = useBuilderStore((s) => s.rootIds);
  const mode = useBuilderStore((s) => s.mode);
  const selectNode = useBuilderStore((s) => s.selectNode);

  const isPreviewMode = mode === "preview";

  const { setNodeRef } = useDroppable({
    id: "root",
    disabled: isPreviewMode,
  });

  const handleBackgroundClick = () => {
    if (!isPreviewMode) {
      selectNode(null);
    }
  };

  return (
    <div
      className="p-8 pt-10 flex justify-center items-start min-h-full"
      onClick={handleBackgroundClick}
    >
      <WindowFrame>
        <div
          ref={isPreviewMode ? undefined : setNodeRef}
          className="p-8 max-w-2xl mx-auto min-h-full relative"
          data-id="root"
        >
          {rootIds.length === 0 ? (
            <EmptyCanvas />
          ) : isPreviewMode ? (
            <PreviewForm />
          ) : (
            <div className="min-h-120 relative">
              <SortableContext
                items={rootIds}
                strategy={verticalListSortingStrategy}
              >
                <HeadlessCanvas
                  nodeRenderer={({ node, content }) => (
                    <NodeWrapper node={node}>{content}</NodeWrapper>
                  )}
                />
              </SortableContext>
            </div>
          )}
        </div>
      </WindowFrame>
    </div>
  );
};

/**
 * Main Form Builder component that orchestrates the entire UI.
 */
const NEW_BUILDER_STORAGE_NAMESPACE = "buzzform-builder:v2:forms";

export const FormBuilder = () => {
  const storageProvider = React.useMemo(() => {
    if (typeof window === "undefined") return null;
    try {
      return getBrowserLocalStorageProvider(NEW_BUILDER_STORAGE_NAMESPACE);
    } catch {
      return null;
    }
  }, []);

  return (
    <DefaultBuilderProvider
      registry={SHADCN_BUILDER_REGISTRY}
      storageProvider={storageProvider}
    >
      <FormBuilderContent storageProvider={storageProvider} />
    </DefaultBuilderProvider>
  );
};

/**
 * Inner component that uses builder hooks.
 */
const FormBuilderContent = ({
  storageProvider,
}: {
  storageProvider: BuilderStorageProvider | null;
}) => {
  const mounted = React.useSyncExternalStore(
    () => () => {},
    () => true,
    () => false,
  );
  const containerRef = useRef<HTMLDivElement>(null);
  const zoom = useBuilderStore((state) => state.zoom);
  const setZoom = useBuilderStore((state) => state.setZoom);
  const mode = useBuilderStore((s) => s.mode);

  const onSubmit = async (data: Record<string, unknown>) => {
    await new Promise((r) => setTimeout(r, 500));
    toast("Form Submitted!", {
      description: <SubmitToastContent data={data} />,
      duration: 10000,
    });
  };

  // Handle Ctrl+Wheel zoom
  useEffect(() => {
    if (!mounted) return;
    const container = containerRef.current;
    if (!container) return;

    const handleWheel = (e: WheelEvent) => {
      if (e.ctrlKey) {
        e.preventDefault();
        const delta = e.deltaY * -0.002;
        const nextZoom = Math.round((zoom + delta) * 100) / 100;
        // Clamp zoom between 0.25 and 2
        const clampedZoom = Math.max(0.25, Math.min(2, nextZoom));
        setZoom(clampedZoom);
      }
    };

    container.addEventListener("wheel", handleWheel, { passive: false });
    return () => container.removeEventListener("wheel", handleWheel);
  }, [zoom, setZoom, mounted]);

  if (!mounted) return null;

  return (
    <BuilderDndProvider
      renderOverlay={({ activeId, activeData }) => (
        <DragOverlayItem activeId={activeId} activeData={activeData} />
      )}
    >
      <BuilderFormProvider mode={mode} onSubmit={onSubmit}>
        <MobileOverlay />
        <div className="flex flex-col h-screen w-full bg-muted/30 overflow-hidden text-foreground">
          {/* Top Header */}
          <SiteHeader storageProvider={storageProvider} />

          {/* Main Layout */}
          <SidebarProvider className="flex-1 overflow-hidden min-h-0 min-w-0">
            <div className="flex h-full w-full">
              {/* Left Sidebar: Components */}
              <div
                className={cn(
                  "border-r bg-background transition-all duration-300 ease-in-out",
                  mode === "preview"
                    ? "w-0 opacity-0 overflow-hidden border-none"
                    : "w-64",
                )}
              >
                <div className="w-64">
                  <FieldSidebar />
                </div>
              </div>

              {/* Central Canvas */}
              <main className="flex-1 flex flex-col relative overflow-hidden bg-muted/20 min-w-0">
                <CanvasToolbar />
                <div ref={containerRef} className="flex-1 min-h-0">
                  <div className="h-full overflow-auto [&::-webkit-scrollbar]:hidden [scrollbar-width:none] bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#1f2937_1px,transparent_1px)] bg-size-[20px_20px]">
                    <CanvasFrame />
                  </div>
                </div>
              </main>

              {/* Right Sidebar: Properties */}
              <aside
                className={cn(
                  "border-l bg-background flex flex-col shrink-0 transition-all duration-300 ease-in-out",
                  mode === "preview"
                    ? "w-0 opacity-0 overflow-hidden border-none"
                    : "w-80",
                )}
              >
                <div className="w-80 h-full flex flex-col">
                  <PropertyPanel />
                </div>
              </aside>
            </div>
          </SidebarProvider>
        </div>
      </BuilderFormProvider>
    </BuilderDndProvider>
  );
};
registry/shadcn/builder/header/cloud-save-dialog.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogFooter,
} from "@/components/ui/dialog";
import { HugeiconsIcon } from "@hugeicons/react";
import { CloudServerIcon } from "@hugeicons/core-free-icons";
import { Sparkles } from "lucide-react";
import { joinWaitlist } from "@/app/(builder)/lib/actions";
import { toast } from "sonner";
import { createSchema, InferType } from "@buildnbuzz/buzzform";
import {
  Form,
  FormContent,
  FormFields,
  FormSubmit,
  FormAction,
} from "@/registry/base/form";
import Link from "next/link";

const waitlistSchema = createSchema([
  {
    type: "email",
    name: "email",
    label: "Email Address",
    placeholder: "you@example.com",
    required: true,
    description: (
      <span className="text-xs">
        By joining, you agree to our{" "}
        <Link
          href="/terms"
          className="underline hover:text-primary underline-offset-4"
        >
          Terms
        </Link>{" "}
        and{" "}
        <Link
          href="/privacy"
          className="underline hover:text-primary underline-offset-4"
        >
          Privacy Policy
        </Link>
        .
      </span>
    ),
  },
]);

export type WaitlistData = InferType<typeof waitlistSchema>;

export function CloudSaveDialog() {
  const [open, setOpen] = useState(false);

  const handleSubmit = async (data: WaitlistData) => {
    try {
      const result = await joinWaitlist(data.email);

      if (result.error) {
        toast.error(result.error);
      } else {
        toast.success(result.message || "You've been added to the waitlist!");
        setOpen(false);
      }
    } catch {
      toast.error("Something went wrong. Please try again.");
    }
  };

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger
        render={
          <Button
            variant="outline"
            size="sm"
            className="gap-2 h-8 hidden sm:flex"
          >
            <HugeiconsIcon icon={CloudServerIcon} size={16} />
            Save
          </Button>
        }
      />
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            <HugeiconsIcon
              icon={CloudServerIcon}
              className="text-primary h-6 w-6"
            />
            Save & Sync
          </DialogTitle>
          <DialogDescription>
            Never lose your work again. Save your forms to the cloud, access
            version history, and create your central form repository.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col">
          <div className="bg-primary/5 border border-primary/20 rounded-lg p-3 text-sm flex gap-3 text-primary mb-6">
            <Sparkles className="h-5 w-5 shrink-0 mt-0.5" />
            <div>
              <p className="font-medium mb-1">Coming Soon</p>
              <p className="text-muted-foreground text-xs">
                We&apos;re currently building the persistence layer. Join the
                waitlist to get <b>early access</b> and{" "}
                <b>free cloud storage</b> when we launch.
              </p>
            </div>
          </div>

          <Form schema={waitlistSchema} onSubmit={handleSubmit}>
            <FormContent className="space-y-4">
              <FormFields />
              <DialogFooter>
                <FormAction
                  variant="ghost"
                  onClick={() => setOpen(false)}
                  type="button"
                >
                  Cancel
                </FormAction>
                <FormSubmit>Join Waitlist</FormSubmit>
              </DialogFooter>
            </FormContent>
          </Form>
        </div>
      </DialogContent>
    </Dialog>
  );
}
registry/shadcn/builder/header/form-manager-dialog.tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { FormManagerNewPanel } from "./form-manager-new-panel";
import { FormManagerSavedPanel } from "./form-manager-saved-panel";
import type { BuilderStorageProvider } from "@buildnbuzz/form-builder-core";

export function FormManagerDialog({
  storageProvider,
}: {
  storageProvider: BuilderStorageProvider | null;
}) {
  const [open, setOpen] = React.useState(false);

  const handleDone = React.useCallback(() => setOpen(false), []);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger
        render={
          <Button className="group gap-2">
            <IconPlaceholder
              hugeicons="Add01Icon"
              lucide="Plus"
              tabler="IconPlus"
              phosphor="Plus"
              remixicon="RiAddLine"
              size={16}
              strokeWidth={2}
              className="transition-transform duration-300 group-hover:rotate-90"
            />
            New
          </Button>
        }
      />

      <DialogContent className="sm:max-w-3xl">
        <DialogHeader>
          <DialogTitle>Forms</DialogTitle>
          <DialogDescription>
            Create a new form, import a file, or open a previously saved form.
          </DialogDescription>
        </DialogHeader>

        <Tabs defaultValue="new">
          <div className="flex justify-center">
            <TabsList className="grid w-full max-w-xs grid-cols-2 rounded-full bg-muted">
              <TabsTrigger value="new" className="gap-2 rounded-full">
                <IconPlaceholder
                  hugeicons="Add01Icon"
                  lucide="Plus"
                  tabler="IconPlus"
                  phosphor="Plus"
                  remixicon="RiAddLine"
                  size={16}
                  strokeWidth={2}
                />
                New
              </TabsTrigger>
              <TabsTrigger value="saved" className="gap-2 rounded-full">
                <IconPlaceholder
                  hugeicons="FolderIcon"
                  lucide="Folder"
                  tabler="IconFolder"
                  phosphor="Folder"
                  remixicon="RiFolderLine"
                  size={16}
                  strokeWidth={2}
                />
                Saved Forms
              </TabsTrigger>
            </TabsList>
          </div>

          <TabsContent value="new">
            <FormManagerNewPanel onDone={handleDone} />
          </TabsContent>

          <TabsContent value="saved">
            <FormManagerSavedPanel
              onDone={handleDone}
              storageProvider={storageProvider}
            />
          </TabsContent>
        </Tabs>
      </DialogContent>
    </Dialog>
  );
}
registry/shadcn/builder/header/form-manager-new-panel.tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { useBuilderStore } from "@buildnbuzz/form-builder-react";
import { toast } from "sonner";
import {
  parseImportedFormJson,
  type ParsedImportPayload,
} from "@buildnbuzz/form-builder-core";

const MIN_IMPORT_FEEDBACK_MS = 500;

type PendingImport = {
  sourceName: string;
  formName: string;
  nodeCount: number;
  state: ParsedImportPayload["state"];
  warnings?: string[];
};

type FormManagerNewPanelProps = {
  onDone: () => void;
};

type FormManagerPendingImportStateProps = {
  pendingImport: PendingImport;
  isImporting: boolean;
  onConfirmImport: () => void;
  onClearSelection: () => void;
};

type FormManagerPasteImportStateProps = {
  value: string;
  isImporting: boolean;
  onChangeValue: (value: string) => void;
  onBack: () => void;
  onPasteFromClipboard: () => void;
  onImport: () => void;
  onDragOver: (event: React.DragEvent<HTMLDivElement>) => void;
  onDrop: (event: React.DragEvent<HTMLDivElement>) => void;
};

function FormManagerPendingImportState({
  pendingImport,
  isImporting,
  onConfirmImport,
  onClearSelection,
}: FormManagerPendingImportStateProps) {
  return (
    <div className="flex w-full flex-1 flex-col animate-in fade-in duration-200">
      <div className="flex flex-1 flex-col items-center justify-center gap-4 py-8">
        <div className="flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 text-primary">
          <IconPlaceholder
            hugeicons="File02Icon"
            lucide="File"
            tabler="IconFile"
            phosphor="File"
            remixicon="RiFileLine"
            size={32}
            strokeWidth={1.5}
          />
        </div>

        <div className="space-y-2 text-center">
          <h3 className="text-xl font-semibold tracking-tight">
            {pendingImport.formName}
          </h3>
          <p className="text-sm text-muted-foreground">
            {pendingImport.sourceName}
          </p>
          <div className="flex items-center justify-center gap-2">
            <span className="text-xs text-muted-foreground">
              {pendingImport.nodeCount} nodes
            </span>
          </div>
        </div>
      </div>

      <div className="mt-auto flex w-full flex-col gap-4 rounded-xl border bg-muted/30 p-4">
        {pendingImport.warnings && pendingImport.warnings.length > 0 && (
          <Alert className="border-amber-500/30 bg-amber-500/10 text-amber-600 dark:border-amber-500/20 dark:text-amber-400">
            <div className="flex items-center gap-2">
              <IconPlaceholder
                hugeicons="Alert01Icon"
                lucide="AlertTriangle"
                tabler="IconAlertTriangle"
                phosphor="Warning"
                remixicon="RiAlertFill"
                size={16}
              />
              <AlertTitle className="m-0 text-sm font-semibold">
                Legacy Schema Migrated
              </AlertTitle>
            </div>
            <AlertDescription className="mt-2 max-h-32 overflow-y-auto">
              <ul className="ml-5 list-disc space-y-1 text-xs opacity-90">
                {pendingImport.warnings.map((warning, idx) => (
                  <li key={idx}>{warning}</li>
                ))}
              </ul>
            </AlertDescription>
          </Alert>
        )}

        <div className="flex items-start gap-3">
          <div className="mt-0.5 text-amber-600 dark:text-amber-500">
            <IconPlaceholder
              hugeicons="Alert01Icon"
              lucide="AlertCircle"
              tabler="IconAlertCircle"
              phosphor="WarningCircle"
              remixicon="RiAlertLine"
              size={18}
              strokeWidth={2}
            />
          </div>
          <div className="flex-1 space-y-0.5">
            <p className="text-sm font-medium leading-none">
              Replace current workspace?
            </p>
            <p className="text-xs text-muted-foreground">
              Continuing will overwrite any unsaved changes.
            </p>
          </div>
        </div>

        <div className="mt-1 grid grid-cols-2 gap-2">
          <Button
            variant="outline"
            onClick={onClearSelection}
            disabled={isImporting}
          >
            Cancel
          </Button>
          <Button onClick={onConfirmImport} disabled={isImporting}>
            <IconPlaceholder
              hugeicons="Upload01Icon"
              lucide="Upload"
              tabler="IconUpload"
              phosphor="UploadSimple"
              remixicon="RiUpload2Line"
              size={16}
              className="mr-2"
              strokeWidth={2}
            />
            Import Now
          </Button>
        </div>
      </div>
    </div>
  );
}

function FormManagerPasteImportState({
  value,
  isImporting,
  onChangeValue,
  onBack,
  onPasteFromClipboard,
  onImport,
  onDragOver,
  onDrop,
}: FormManagerPasteImportStateProps) {
  return (
    <div className="flex min-h-0 w-full flex-1 flex-col gap-3 animate-in fade-in duration-200">
      <div className="space-y-1">
        <h3 className="text-lg font-semibold tracking-tight">Import JSON</h3>
        <p className="text-sm text-muted-foreground">
          Paste a BuzzForm schema or Builder backup JSON.
        </p>
      </div>

      <div
        className="h-56 min-h-0 overflow-hidden rounded-lg border border-dashed border-border/70 p-2 transition-colors hover:border-primary/50"
        onDragOver={onDragOver}
        onDrop={onDrop}
      >
        <Textarea
          value={value}
          onChange={(event) => onChangeValue(event.target.value)}
          placeholder='[\n  { "type": "text", "name": "email", "label": "Email" }\n]'
          wrap="off"
          className="h-full min-h-0 resize-none overflow-x-auto overflow-y-auto border-0 bg-transparent font-mono text-xs whitespace-pre shadow-none field-sizing-fixed focus-visible:ring-0"
        />
      </div>

      <div className="mt-auto flex flex-wrap justify-end gap-2">
        <Button variant="ghost" onClick={onBack} disabled={isImporting}>
          Back
        </Button>
        <Button
          variant="outline"
          onClick={onPasteFromClipboard}
          disabled={isImporting}
        >
          <IconPlaceholder
            hugeicons="Copy01Icon"
            lucide="Copy"
            tabler="IconCopy"
            phosphor="Copy"
            remixicon="RiFileCopyLine"
            size={16}
            className="mr-2"
            strokeWidth={2}
          />
          Paste from Clipboard
        </Button>
        <Button
          onClick={onImport}
          disabled={isImporting || value.trim().length === 0}
        >
          {isImporting ? (
            <Spinner className="mr-2 size-4" />
          ) : (
            <IconPlaceholder
              hugeicons="Upload01Icon"
              lucide="Upload"
              tabler="IconUpload"
              phosphor="UploadSimple"
              remixicon="RiUpload2Line"
              size={16}
              className="mr-2"
              strokeWidth={2}
            />
          )}
          Validate JSON
        </Button>
      </div>
    </div>
  );
}

export function FormManagerNewPanel({ onDone }: FormManagerNewPanelProps) {
  const [isImporting, setIsImporting] = React.useState(false);
  const [pendingImport, setPendingImport] =
    React.useState<PendingImport | null>(null);
  const [importError, setImportError] = React.useState<string | null>(null);
  const [isPasteMode, setIsPasteMode] = React.useState(false);
  const [pastedJson, setPastedJson] = React.useState("");

  const fileInputRef = React.useRef<HTMLInputElement | null>(null);
  const clearState = useBuilderStore((state) => state.clearState);
  const loadDocumentState = useBuilderStore((state) => state.loadDocumentState);

  const resetImportState = React.useCallback(() => {
    setIsImporting(false);
    setPendingImport(null);
    setImportError(null);
  }, []);

  const handleNewForm = () => {
    clearState();
    onDone();
  };

  const handleConfirmImport = () => {
    if (!pendingImport) return;
    loadDocumentState(
      pendingImport.state as Parameters<typeof loadDocumentState>[0],
    );
    toast.success(`Imported "${pendingImport.formName}"`);
    onDone();
  };

  const handleImportClick = () => {
    if (isImporting) return;
    fileInputRef.current?.click();
  };

  const handleOpenPasteMode = () => {
    if (isImporting) return;
    setImportError(null);
    setIsPasteMode(true);
  };

  const handleClosePasteMode = () => {
    if (isImporting) return;
    setImportError(null);
    setIsPasteMode(false);
  };

  const processImportJson = React.useCallback(
    async ({
      json,
      sourceName,
      schemaFileNameHint,
      closePasteModeOnSuccess,
    }: {
      json: string;
      sourceName: string;
      schemaFileNameHint?: string;
      closePasteModeOnSuccess: boolean;
    }) => {
      const startedAt = Date.now();
      setImportError(null);
      setPendingImport(null);
      setIsImporting(true);

      try {
        const parsed = parseImportedFormJson(json);
        const importedState =
          schemaFileNameHint && parsed.state.formName === "Imported Form"
            ? {
                ...parsed.state,
                formName: schemaFileNameHint,
              }
            : parsed.state;

        setPendingImport({
          sourceName,
          formName: importedState.formName,
          nodeCount: Object.keys(importedState.nodes).length,
          state: importedState,
          warnings: parsed.warnings,
        });

        if (closePasteModeOnSuccess) {
          setIsPasteMode(false);
        }
      } catch (error) {
        const rawMessage =
          error instanceof Error ? error.message : "Failed to import JSON.";
        const message = `Invalid import JSON: ${rawMessage}`;
        setImportError(message);
        toast.error(message);
      } finally {
        const elapsed = Date.now() - startedAt;
        if (elapsed < MIN_IMPORT_FEEDBACK_MS) {
          await new Promise((resolve) =>
            setTimeout(resolve, MIN_IMPORT_FEEDBACK_MS - elapsed),
          );
        }
        setIsImporting(false);
      }
    },
    [],
  );

  const handleFileImport = async (
    event: React.ChangeEvent<HTMLInputElement>,
  ) => {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (!file) return;

    const contents = await file.text();
    await processImportJson({
      json: contents,
      sourceName: file.name,
      schemaFileNameHint: toFormNameHint(file.name),
      closePasteModeOnSuccess: false,
    });
  };

  const handleImportPastedJson = async () => {
    const trimmedJson = pastedJson.trim();
    if (!trimmedJson) {
      const message = "Paste JSON content before importing.";
      setImportError(message);
      toast.error(message);
      return;
    }

    await processImportJson({
      json: trimmedJson,
      sourceName: "Pasted JSON",
      closePasteModeOnSuccess: true,
    });
  };

  const handlePasteFromClipboard = async () => {
    if (typeof navigator === "undefined" || !navigator.clipboard?.readText) {
      toast.error("Clipboard access is not available. Paste manually.");
      return;
    }

    try {
      const clipboardText = await navigator.clipboard.readText();
      if (!clipboardText.trim()) {
        toast.error("Clipboard is empty.");
        return;
      }

      setPastedJson(clipboardText);
      setImportError(null);
      toast.success("Pasted JSON from clipboard");
    } catch {
      toast.error("Clipboard access was denied. Paste JSON manually.");
    }
  };

  const handlePasteDropZoneDragOver = (
    event: React.DragEvent<HTMLDivElement>,
  ) => {
    event.preventDefault();
  };

  const handlePasteDropZoneDrop = async (
    event: React.DragEvent<HTMLDivElement>,
  ) => {
    event.preventDefault();
    if (isImporting) return;

    const file = event.dataTransfer.files?.[0];
    if (file) {
      try {
        const droppedContents = await file.text();
        if (!droppedContents.trim()) {
          toast.error("Dropped file is empty.");
          return;
        }
        setPastedJson(droppedContents);
        setImportError(null);
        toast.success(`Loaded JSON from "${file.name}"`);
      } catch {
        toast.error("Failed to read dropped file.");
      }
      return;
    }

    const droppedText = event.dataTransfer.getData("text/plain");
    if (!droppedText.trim()) {
      toast.error("Dropped content is empty.");
      return;
    }

    setPastedJson(droppedText);
    setImportError(null);
    toast.success("Loaded dropped text");
  };

  React.useEffect(() => resetImportState, [resetImportState]);

  const hasPendingImport =
    pendingImport !== null && !isImporting && !importError;

  return (
    <div className="flex min-h-80 flex-col gap-3">
      <div className="flex flex-1 p-4">
        {hasPendingImport ? (
          <FormManagerPendingImportState
            pendingImport={pendingImport}
            isImporting={isImporting}
            onConfirmImport={handleConfirmImport}
            onClearSelection={resetImportState}
          />
        ) : isPasteMode ? (
          <FormManagerPasteImportState
            value={pastedJson}
            isImporting={isImporting}
            onChangeValue={setPastedJson}
            onBack={handleClosePasteMode}
            onPasteFromClipboard={handlePasteFromClipboard}
            onImport={handleImportPastedJson}
            onDragOver={handlePasteDropZoneDragOver}
            onDrop={handlePasteDropZoneDrop}
          />
        ) : (
          <div className="grid w-full flex-1 gap-4 sm:grid-cols-3">
            <Card
              className="group flex cursor-pointer flex-col justify-center transition-all duration-200 hover:ring-1 hover:ring-primary/50"
              onClick={handleNewForm}
            >
              <CardContent className="flex justify-center pb-2 pt-6">
                <IconPlaceholder
                  hugeicons="Add01Icon"
                  lucide="Plus"
                  tabler="IconPlus"
                  phosphor="Plus"
                  remixicon="RiAddLine"
                  size={32}
                  strokeWidth={1.5}
                  className="text-muted-foreground transition-all duration-300 group-hover:scale-110 group-hover:rotate-90 group-hover:text-primary"
                />
              </CardContent>
              <CardHeader className="justify-center text-center">
                <CardTitle className="text-lg">Start Blank</CardTitle>
                <CardDescription>Create a fresh empty form</CardDescription>
              </CardHeader>
            </Card>

            <Card
              className="group flex cursor-pointer flex-col justify-center transition-all duration-200 hover:ring-1 hover:ring-primary/50"
              onClick={handleImportClick}
            >
              <CardContent className="flex justify-center pb-2 pt-6">
                {isImporting ? (
                  <div className="flex items-center justify-center p-2 text-muted-foreground transition-colors group-hover:text-primary">
                    <Spinner className="size-10" />
                  </div>
                ) : (
                  <IconPlaceholder
                    hugeicons="Upload01Icon"
                    lucide="Upload"
                    tabler="IconUpload"
                    phosphor="UploadSimple"
                    remixicon="RiUpload2Line"
                    size={32}
                    strokeWidth={1.5}
                    className="text-muted-foreground transition-all duration-300 group-hover:-translate-y-1 group-hover:scale-110 group-hover:text-primary"
                  />
                )}
              </CardContent>
              <CardHeader className="justify-center text-center">
                <CardTitle className="text-lg">
                  {isImporting ? "Validating File" : "Import File"}
                </CardTitle>
                <CardDescription>
                  {isImporting
                    ? "Checking JSON before import"
                    : "Load .json from disk"}
                </CardDescription>
              </CardHeader>
            </Card>

            <Card
              className="group flex cursor-pointer flex-col justify-center transition-all duration-200 hover:ring-1 hover:ring-primary/50"
              onClick={handleOpenPasteMode}
            >
              <CardContent className="flex justify-center pb-2 pt-6">
                <IconPlaceholder
                  hugeicons="Copy01Icon"
                  lucide="Copy"
                  tabler="IconCopy"
                  phosphor="Copy"
                  remixicon="RiFileCopyLine"
                  size={32}
                  strokeWidth={1.5}
                  className="text-muted-foreground transition-all duration-300 group-hover:-translate-y-1 group-hover:scale-110 group-hover:text-primary"
                />
              </CardContent>
              <CardHeader className="justify-center text-center">
                <CardTitle className="text-lg">Import JSON</CardTitle>
                <CardDescription>Paste schema from clipboard</CardDescription>
              </CardHeader>
            </Card>
          </div>
        )}
      </div>

      {importError && (
        <Alert variant="destructive">
          <IconPlaceholder
            hugeicons="Alert01Icon"
            lucide="AlertCircle"
            tabler="IconAlertCircle"
            phosphor="WarningCircle"
            remixicon="RiAlertLine"
            size={16}
          />
          <AlertTitle>Import Failed</AlertTitle>
          <AlertDescription>{importError}</AlertDescription>
        </Alert>
      )}

      <input
        ref={fileInputRef}
        type="file"
        accept=".json,application/json"
        className="hidden"
        onChange={handleFileImport}
      />
    </div>
  );
}

function toFormNameHint(sourceName: string): string | undefined {
  const normalizedBase = sourceName
    .replace(/\.[^/.]+$/, "")
    .replace(/[_-]+/g, " ")
    .replace(/\s+/g, " ")
    .trim();

  if (normalizedBase.length === 0) {
    return undefined;
  }

  return normalizedBase
    .split(" ")
    .map((segment) => {
      if (segment.length === 0) return segment;
      return segment[0]!.toUpperCase() + segment.slice(1).toLowerCase();
    })
    .join(" ");
}
registry/shadcn/builder/header/form-manager-saved-panel.tsx
"use client";

import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { IconPlaceholder } from "@/components/icon-placeholder";
import type {
  FormSummary,
  BuilderStorageProvider,
} from "@buildnbuzz/form-builder-core";
import { toSafeFileName, fieldsToBuilderState } from "@buildnbuzz/form-builder-core";
import { useBuilderStore, downloadTextFile } from "@buildnbuzz/form-builder-react";
import type { Field } from "@buildnbuzz/form-core";
import { toast } from "sonner";

const dateFormatter = new Intl.DateTimeFormat(undefined, {
  dateStyle: "medium",
  timeStyle: "short",
});

type FormManagerSavedPanelProps = {
  onDone: () => void;
  storageProvider: BuilderStorageProvider | null;
};

export function FormManagerSavedPanel({
  onDone,
  storageProvider,
}: FormManagerSavedPanelProps) {
  const [forms, setForms] = React.useState<FormSummary[]>([]);
  const [isLoading, setIsLoading] = React.useState(false);
  const [loadError, setLoadError] = React.useState<string | null>(null);
  const [openingFormId, setOpeningFormId] = React.useState<string | null>(null);
  const [removingFormId, setRemovingFormId] = React.useState<string | null>(
    null,
  );
  const [downloadingFormId, setDownloadingFormId] = React.useState<
    string | null
  >(null);

  const loadDocumentState = useBuilderStore((state) => state.loadDocumentState);
  const currentFormId = useBuilderStore((state) => state.formId);

  const loadForms = React.useCallback(async () => {
    setIsLoading(true);
    setLoadError(null);

    try {
      if (!storageProvider) return;
      const items = await storageProvider.list();
      setForms(items);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "Failed to load forms.";
      setLoadError(message);
    } finally {
      setIsLoading(false);
    }
  }, [storageProvider]);

  React.useEffect(() => {
    void loadForms();
  }, [loadForms]);

  const handleOpenForm = async (formId: string) => {
    setOpeningFormId(formId);

    try {
      if (!storageProvider) return;
      const document = await storageProvider.load(formId);
      if (!document) throw new Error("Document not found");
      const { nodes, rootIds } = fieldsToBuilderState(
        document.fields as readonly Field[],
      );

      loadDocumentState({
        nodes,
        rootIds,
        formId: document.id || formId,
        formName: document.title || "Untitled",
      });
      onDone();
      toast.success(`Opened "${document.title || "Untitled"}"`);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "Failed to open form.";
      toast.error(message);
    } finally {
      setOpeningFormId(null);
    }
  };

  const handleRemoveForm = async (form: FormSummary) => {
    if (
      !window.confirm(
        `Remove "${form.formName}" from local saved forms? This does not affect the currently open editor until you switch forms.`,
      )
    ) {
      return;
    }

    setRemovingFormId(form.formId);

    try {
      if (!storageProvider) return;
      await storageProvider.remove(form.formId);
      setForms((prev) => prev.filter((item) => item.formId !== form.formId));
      toast.success(`Removed "${form.formName}"`);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "Failed to remove form.";
      toast.error(message);
    } finally {
      setRemovingFormId(null);
    }
  };

  const handleDownloadForm = async (form: FormSummary) => {
    setDownloadingFormId(form.formId);

    try {
      if (!storageProvider) return;
      const document = await storageProvider.load(form.formId);
      if (!document) throw new Error("Document not found");
      const fileName = `${toSafeFileName(document.title || "Untitled")}.json`;

      downloadTextFile(
        JSON.stringify(document, null, 2),
        fileName,
        "application/json",
      );
      toast.success(`Downloaded "${document.title || "Untitled"}"`);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "Failed to download form.";
      toast.error(message);
    } finally {
      setDownloadingFormId(null);
    }
  };

  return (
    <div className="flex min-h-80 flex-col gap-3">
      {isLoading ? (
        <div className="flex h-64 items-center justify-center gap-2 text-sm text-muted-foreground">
          <Spinner className="size-4" />
          Loading saved forms...
        </div>
      ) : loadError ? (
        <div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
          <p className="text-sm text-destructive">{loadError}</p>
          <Button type="button" variant="outline" size="sm" onClick={loadForms}>
            Retry
          </Button>
        </div>
      ) : forms.length === 0 ? (
        <Empty className="min-h-64">
          <EmptyHeader>
            <EmptyMedia variant="icon">
              <IconPlaceholder
                hugeicons="FolderIcon"
                lucide="Folder"
                tabler="IconFolder"
                phosphor="Folder"
                remixicon="RiFolderLine"
                size={16}
                strokeWidth={2}
              />
            </EmptyMedia>
            <EmptyTitle>No saved forms yet</EmptyTitle>
            <EmptyDescription>
              Forms appear here after you edit and save them locally.
            </EmptyDescription>
          </EmptyHeader>
        </Empty>
      ) : (
        <>
          <div className="rounded-lg border">
            <ScrollArea className="h-72 w-full">
              <div className="space-y-2 p-2">
                {forms.map((form) => {
                  const isOpening = openingFormId === form.formId;
                  const isRemoving = removingFormId === form.formId;
                  const isDownloading = downloadingFormId === form.formId;
                  const isBusy = isOpening || isRemoving || isDownloading;
                  const isCurrent = currentFormId === form.formId;

                  return (
                    <div
                      key={form.formId}
                      className="flex items-start gap-3 rounded-md border p-3"
                    >
                      <div className="mt-0.5 rounded-md border bg-muted p-1.5">
                        <IconPlaceholder
                          hugeicons="InputShortTextIcon"
                          lucide="Type"
                          tabler="IconLetterT"
                          phosphor="TextT"
                          remixicon="RiText"
                          size={14}
                          strokeWidth={2}
                        />
                      </div>

                      <div className="min-w-0 flex-1">
                        <div className="flex items-center gap-2">
                          <p className="truncate text-sm font-medium">
                            {form.formName}
                          </p>
                          {isCurrent && (
                            <Badge variant="secondary">Current</Badge>
                          )}
                        </div>

                        <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
                          <IconPlaceholder
                            hugeicons="Clock01Icon"
                            lucide="Clock"
                            tabler="IconClock"
                            phosphor="Clock"
                            remixicon="RiTimeLine"
                            size={12}
                            strokeWidth={2}
                          />
                          <span>{dateFormatter.format(form.updatedAt)}</span>
                        </div>
                      </div>

                      <div className="flex items-center gap-1">
                        <Button
                          type="button"
                          variant="outline"
                          size="sm"
                          className="gap-1.5"
                          disabled={isBusy}
                          onClick={() => handleOpenForm(form.formId)}
                        >
                          {isOpening ? (
                            <Spinner className="size-3.5" />
                          ) : (
                            <IconPlaceholder
                              hugeicons="ArrowRight01Icon"
                              lucide="ArrowRight"
                              tabler="IconArrowRight"
                              phosphor="ArrowRight"
                              remixicon="RiArrowRightLine"
                              size={14}
                              strokeWidth={2}
                            />
                          )}
                          Open
                        </Button>

                        <Tooltip>
                          <TooltipTrigger
                            render={
                              <Button
                                type="button"
                                variant="ghost"
                                size="icon-sm"
                                disabled={isBusy}
                                onClick={() => handleDownloadForm(form)}
                                className="text-muted-foreground"
                              >
                                {isDownloading ? (
                                  <Spinner className="size-3.5" />
                                ) : (
                                  <IconPlaceholder
                                    hugeicons="Download02Icon"
                                    lucide="Download"
                                    tabler="IconDownload"
                                    phosphor="DownloadSimple"
                                    remixicon="RiDownload2Line"
                                    size={14}
                                    strokeWidth={2}
                                  />
                                )}
                                <span className="sr-only">
                                  Download saved form
                                </span>
                              </Button>
                            }
                          />
                          <TooltipContent side="top">Download</TooltipContent>
                        </Tooltip>

                        <Tooltip>
                          <TooltipTrigger
                            render={
                              <Button
                                type="button"
                                variant="ghost"
                                size="icon-sm"
                                disabled={isBusy}
                                onClick={() => handleRemoveForm(form)}
                                className="text-muted-foreground hover:text-destructive"
                              >
                                {isRemoving ? (
                                  <Spinner className="size-3.5" />
                                ) : (
                                  <IconPlaceholder
                                    hugeicons="Delete02Icon"
                                    lucide="Trash2"
                                    tabler="IconTrash"
                                    phosphor="Trash"
                                    remixicon="RiDeleteBinLine"
                                    size={14}
                                    strokeWidth={2}
                                  />
                                )}
                                <span className="sr-only">
                                  Remove saved form
                                </span>
                              </Button>
                            }
                          />
                          <TooltipContent side="top">Delete</TooltipContent>
                        </Tooltip>
                      </div>
                    </div>
                  );
                })}
              </div>
            </ScrollArea>
          </div>

          <div className="flex justify-start">
            <Button
              type="button"
              variant="outline"
              size="sm"
              className="gap-2"
              onClick={loadForms}
            >
              <IconPlaceholder
                hugeicons="Refresh01Icon"
                lucide="RefreshCw"
                tabler="IconRefresh"
                phosphor="ArrowsClockwise"
                remixicon="RiRefreshLine"
                size={14}
                strokeWidth={2}
              />
              Refresh
            </Button>
          </div>
        </>
      )}
    </div>
  );
}
registry/shadcn/builder/header/index.tsx
"use client";

import Image from "next/image";
import Link from "next/link";
import { ThemeToggle } from "@/components/theme-toggle";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { useBuilderStore } from "@buildnbuzz/form-builder-react";

import { SaveIndicator } from "./save-indicator";
import { FormManagerDialog } from "./form-manager-dialog";
import { CloudSaveDialog } from "./cloud-save-dialog";
import { ExportSheet } from "../export-sheet";
import type { BuilderStorageProvider } from "@buildnbuzz/form-builder-core";

export function SiteHeader({
  storageProvider,
}: {
  storageProvider: BuilderStorageProvider | null;
}) {
  const hasContent = useBuilderStore((state) => state.rootIds.length > 0);

  return (
    <header className="bg-background sticky top-0 z-50 flex w-full items-center border-b">
      <div className="flex h-header w-full items-center gap-2 px-4 text-foreground">
        <div className="flex min-w-0 items-center gap-2">
          <Link href="/" className="flex items-center gap-2">
            <div className="flex aspect-square size-8 items-center justify-center rounded-lg invert dark:invert-0">
              <Image src="/bb-icon.svg" alt="Logo" width={24} height={24} />
            </div>
            <span className="text-sm font-semibold tracking-tight">
              BuzzForm
            </span>
            <Badge
              variant="secondary"
              className="h-5 rounded-full px-2 text-[10px] font-semibold tracking-wide"
            >
              Preview
            </Badge>
          </Link>
        </div>

        <div className="ml-auto flex items-center gap-4">
          <SaveIndicator />
          {hasContent && (
            <Separator
              orientation="vertical"
              className="h-5 data-[orientation=vertical]:self-center"
            />
          )}
          <div className="flex items-center gap-2">
            <ThemeToggle />
            <CloudSaveDialog />
            <ExportSheet />
            <FormManagerDialog storageProvider={storageProvider} />
          </div>
        </div>
      </div>
    </header>
  );
}
registry/shadcn/builder/header/save-indicator.tsx
"use client";

import { useBuilderStore } from "@buildnbuzz/form-builder-react";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { cn } from "@/lib/utils";

function formatTime(timestamp: number): string {
  return new Date(timestamp).toLocaleTimeString([], {
    hour: "numeric",
    minute: "2-digit",
  });
}

export function SaveIndicator() {
  const saveStatus = useBuilderStore((state) => state.saveStatus);
  const lastSavedAt = useBuilderStore((state) => state.lastSavedAt);
  const hasContent = useBuilderStore((state) => state.rootIds.length > 0);

  if (!hasContent) return null;

  return (
    <div
      className={cn(
        "flex items-center gap-1.5 text-xs text-muted-foreground transition-opacity duration-200",
        saveStatus === "idle" && !lastSavedAt && "opacity-0",
      )}
    >
      {saveStatus === "saving" ? (
        <>
          <IconPlaceholder
            hugeicons="Loading03Icon"
            lucide="Loader2"
            tabler="IconLoader2"
            phosphor="CircleNotch"
            remixicon="RiLoader2Line"
            size={14}
            className="animate-spin"
          />
          <span>Saving...</span>
        </>
      ) : lastSavedAt ? (
        <>
          <IconPlaceholder
            hugeicons="CheckmarkCircle01Icon"
            lucide="CheckCircle2"
            tabler="IconCircleCheck"
            phosphor="CheckCircle"
            remixicon="RiCheckboxCircleLine"
            size={14}
            className="text-emerald-500"
          />
          <span>Last saved at {formatTime(lastSavedAt)}</span>
        </>
      ) : null}
    </div>
  );
}
registry/shadcn/builder/layout/array-renderer.tsx
"use client";

import { useEffect, useRef } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Badge } from "@/components/ui/badge";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  DEFAULT_SLOT,
  useBuilderStore,
  type BuilderNodeRendererProps,
} from "@buildnbuzz/form-builder-react";
import type { ArrayField } from "@buildnbuzz/form-core";

/**
 * Visual renderer for 'array' layout fields.
 * Wraps repeating items with min/max indicator limits securely resolving DnD sorting wrappers context.
 */
export const ArrayRenderer = ({
  id,
  field,
  childrenIds,
  renderSlot,
}: BuilderNodeRendererProps) => {
  const arrayField = field as ArrayField;
  const rawField = arrayField as unknown as Record<string, unknown>;
  const uiConfig = (arrayField.ui as Record<string, unknown> | undefined) ?? {};
  const defaultCollapsed = (uiConfig.collapsed as boolean | undefined) ?? false;
  const minItems = arrayField.minItems;
  const maxItems = arrayField.maxItems;

  const isCollapsed = useBuilderStore(
    (s) => s.collapsedNodes[id] ?? defaultCollapsed,
  );
  const toggleCollapsed = useBuilderStore((s) => s.toggleCollapsed);
  const setCollapsed = useBuilderStore((s) => s.setCollapsed);

  const initializedRef = useRef(false);
  useEffect(() => {
    if (!initializedRef.current) {
      initializedRef.current = true;
      if (defaultCollapsed) {
        setCollapsed(id, true);
      }
    }
  }, [id, defaultCollapsed, setCollapsed]);

  const { setNodeRef, isOver } = useDroppable({
    id: `${id}-dropzone`,
    data: { type: "array", parentId: id, parentSlot: DEFAULT_SLOT },
  });

  const isEmpty = childrenIds.length === 0;

  const renderDropZone = () => (
    <div
      ref={setNodeRef}
      data-container-padding
      className={cn("min-h-16 transition-colors", isOver && "bg-primary/5")}
    >
      {isEmpty ? (
        <div
          className={cn(
            "h-16 flex items-center justify-center rounded-lg border-2 border-dashed transition-colors",
            isOver
              ? "border-primary/50 bg-primary/5"
              : "border-muted-foreground/20 bg-muted/30",
          )}
        >
          <span className="text-muted-foreground text-sm italic">
            Drop fields for each array item
          </span>
        </div>
      ) : (
        <div className="space-y-3">{renderSlot(DEFAULT_SLOT)}</div>
      )}
    </div>
  );

  return (
    <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
      <Card className="w-full py-0 gap-0 border-dashed">
        <CardHeader className="p-0 border-b-0">
          <CollapsibleTrigger
            className={cn(
              "w-full px-4 py-3 flex flex-row items-center justify-between",
              "hover:bg-muted/75 bg-muted/50 transition-colors select-none cursor-pointer",
              !isCollapsed && "border-b",
            )}
          >
            <div className="flex min-w-0 flex-1 items-center gap-2">
              <IconPlaceholder
                lucide="Menu"
                hugeicons="Menu01Icon"
                size={16}
                className="text-muted-foreground shrink-0"
              />
              <span className="text-sm font-semibold truncate">
                {(rawField.label as string) || "Array"}
              </span>
              <Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
                {childrenIds.length}{" "}
                {childrenIds.length === 1 ? "field" : "fields"}
              </Badge>
              {typeof minItems === "number" && (
                <Badge
                  variant="outline"
                  className="h-5 px-1.5 text-[10px] text-muted-foreground"
                >
                  min {minItems}
                </Badge>
              )}
              {typeof maxItems === "number" && (
                <Badge
                  variant="outline"
                  className="h-5 px-1.5 text-[10px] text-muted-foreground"
                >
                  max {maxItems}
                </Badge>
              )}
            </div>

            <IconPlaceholder
              lucide="ChevronDown"
              hugeicons="ArrowDown01Icon"
              size={16}
              className={cn(
                "text-muted-foreground transition-transform duration-200 shrink-0",
                isCollapsed && "-rotate-90",
              )}
            />
          </CollapsibleTrigger>
        </CardHeader>

        <CollapsibleContent>
          <CardContent className="p-4">
            {!!rawField.description && (
              <p className="text-sm text-muted-foreground mb-3">
                {rawField.description as string}
              </p>
            )}
            {renderDropZone()}
          </CardContent>
        </CollapsibleContent>
      </Card>
    </Collapsible>
  );
};
registry/shadcn/builder/layout/collapsible-renderer.tsx
"use client";

import { useEffect, useRef } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  DEFAULT_SLOT,
  useBuilderStore,
  type BuilderNodeRendererProps,
} from "@buildnbuzz/form-builder-react";

const spacingMap = {
  sm: "space-y-2",
  md: "space-y-3",
  lg: "space-y-4",
};

/**
 * Visual renderer for 'collapsible' container fields.
 * Includes multiple UI variants natively integrated with builder store state tracking.
 */
export const CollapsibleRenderer = ({
  id,
  field,
  childrenIds,
  renderSlot,
}: BuilderNodeRendererProps) => {
  const rawField = field as unknown as Record<string, unknown>;
  const uiConfig = (rawField.ui as Record<string, unknown> | undefined) ?? {};

  const label = rawField.label as string | undefined;
  const variant =
    (uiConfig.variant as "ghost" | "bordered" | "card" | "flat" | undefined) ??
    "bordered";
  const spacingConfig = uiConfig.spacing as string | undefined;
  const spacingExtracted =
    spacingConfig && spacingConfig in spacingMap
      ? (spacingConfig as keyof typeof spacingMap)
      : "md";

  const defaultCollapsed = (rawField.collapsed as boolean | undefined) ?? false;

  const isCollapsed = useBuilderStore(
    (s) => s.collapsedNodes[id] ?? defaultCollapsed,
  );
  const toggleCollapsed = useBuilderStore((s) => s.toggleCollapsed);
  const setCollapsed = useBuilderStore((s) => s.setCollapsed);

  const initializedRef = useRef(false);
  useEffect(() => {
    if (!initializedRef.current) {
      initializedRef.current = true;
      if (defaultCollapsed) {
        setCollapsed(id, true);
      }
    }
  }, [id, defaultCollapsed, setCollapsed]);

  const { setNodeRef, isOver } = useDroppable({
    id: `${id}-dropzone`,
    data: { type: "collapsible", parentId: id, parentSlot: DEFAULT_SLOT },
  });

  const isEmpty = childrenIds.length === 0;

  const renderDropZone = () => (
    <div
      ref={setNodeRef}
      data-container-padding
      className={cn("min-h-16 transition-colors", isOver && "bg-primary/5")}
    >
      {isEmpty ? (
        <div
          className={cn(
            "h-16 flex items-center justify-center rounded-lg border-2 border-dashed transition-colors",
            isOver
              ? "border-primary/50 bg-primary/5"
              : "border-muted-foreground/20 bg-muted/30",
          )}
        >
          <span className="text-muted-foreground text-sm italic">
            Drop fields here
          </span>
        </div>
      ) : (
        <div className={cn(spacingMap[spacingExtracted])}>
          {renderSlot(DEFAULT_SLOT)}
        </div>
      )}
    </div>
  );

  const renderHeaderContent = () => (
    <div className="flex-1 min-w-0 flex items-center gap-2">
      <IconPlaceholder
        lucide="FoldVertical"
        hugeicons="ArrowShrink02Icon"
        size={16}
        className="text-muted-foreground shrink-0"
      />
      <span
        className={cn(
          "text-sm truncate",
          variant === "card" ? "font-semibold" : "font-medium",
        )}
      >
        {label || "Collapsible"}
      </span>
      {!isEmpty && (
        <span className="text-xs text-muted-foreground">
          ({childrenIds.length} {childrenIds.length === 1 ? "field" : "fields"})
        </span>
      )}
    </div>
  );

  const renderChevron = (size: number = 16) => (
    <IconPlaceholder
      lucide="ChevronDown"
      hugeicons="ArrowDown01Icon"
      size={size}
      className={cn(
        "text-muted-foreground transition-transform duration-200 shrink-0",
        isCollapsed && "-rotate-90",
      )}
    />
  );

  // === GHOST VARIANT ===
  if (variant === "ghost") {
    return (
      <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
        <div className="w-full">
          <CollapsibleTrigger className="w-full px-2 py-2 rounded-md flex flex-row items-center justify-between hover:bg-muted/50 transition-colors select-none cursor-pointer">
            {renderHeaderContent()}
            {renderChevron(14)}
          </CollapsibleTrigger>
          <CollapsibleContent>
            <div className="pt-3 pl-2">{renderDropZone()}</div>
          </CollapsibleContent>
        </div>
      </Collapsible>
    );
  }

  // === BORDERED VARIANT (DEFAULT) ===
  if (variant === "bordered" || variant === "flat") {
    return (
      <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
        <div className="w-full border border-dashed border-border rounded-lg overflow-hidden">
          <CollapsibleTrigger className="w-full px-4 py-2 flex flex-row items-center justify-between hover:bg-muted/50 transition-colors select-none cursor-pointer">
            {renderHeaderContent()}
            {renderChevron(14)}
          </CollapsibleTrigger>
          <CollapsibleContent>
            <div className="px-4 pt-2 pb-4">{renderDropZone()}</div>
          </CollapsibleContent>
        </div>
      </Collapsible>
    );
  }

  // === CARD VARIANT ===
  return (
    <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
      <Card className="w-full py-0 gap-0">
        <CardHeader className="p-0 border-b-0">
          <CollapsibleTrigger
            className={cn(
              "w-full px-4 py-3 flex flex-row items-center justify-between",
              "hover:bg-muted/75 bg-muted/50 transition-colors select-none cursor-pointer",
              !isCollapsed && "border-b",
            )}
          >
            {renderHeaderContent()}
            {renderChevron()}
          </CollapsibleTrigger>
        </CardHeader>
        <CollapsibleContent>
          <CardContent className="p-4">{renderDropZone()}</CardContent>
        </CollapsibleContent>
      </Card>
    </Collapsible>
  );
};
registry/shadcn/builder/layout/group-renderer.tsx
"use client";

import { useEffect, useRef } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { DEFAULT_SLOT, useBuilderStore, type BuilderNodeRendererProps } from "@buildnbuzz/form-builder-react";
import type { GroupField } from "@buildnbuzz/form-core";

const spacingMap = {
  sm: "space-y-2",
  md: "space-y-3",
  lg: "space-y-4",
} as const;

/**
 * Visual renderer for 'group' container fields.
 */
export const GroupRenderer = ({ id, field, renderSlot, childrenIds }: BuilderNodeRendererProps) => {
  const groupField = field as GroupField;
  const { label } = groupField;
  const variant = groupField.ui?.variant ?? "card";
  const spacing = (groupField.ui?.spacing as keyof typeof spacingMap) ?? "md";
  const defaultCollapsed = groupField.ui?.collapsed ?? false;

  const isCollapsed = useBuilderStore(
    (s) => s.collapsedNodes[id] ?? defaultCollapsed,
  );
  const toggleCollapsed = useBuilderStore((s) => s.toggleCollapsed);
  const setCollapsed = useBuilderStore((s) => s.setCollapsed);

  const initializedRef = useRef(false);
  useEffect(() => {
    if (!initializedRef.current) {
      initializedRef.current = true;
      if (defaultCollapsed) {
        setCollapsed(id, true);
      }
    }
  }, [id, defaultCollapsed, setCollapsed]);

  const { setNodeRef, isOver } = useDroppable({
    id: `${id}-dropzone`,
    data: { type: "group", parentId: id, parentSlot: DEFAULT_SLOT },
  });

  const isEmpty = childrenIds.length === 0;

  const renderDropZone = () => (
    <div
      ref={setNodeRef}
      data-container-padding
      className={cn("min-h-16 transition-colors", isOver && "bg-primary/5")}
    >
      {isEmpty ? (
        <div
          className={cn(
            "h-16 flex items-center justify-center rounded-lg border-2 border-dashed transition-colors",
            isOver
              ? "border-primary/50 bg-primary/5"
              : "border-muted-foreground/20 bg-muted/30",
          )}
        >
          <span className="text-muted-foreground text-sm italic">
            Drop fields here
          </span>
        </div>
      ) : (
        <div className={cn(spacingMap[spacing])}>
          {renderSlot(DEFAULT_SLOT)}
        </div>
      )}
    </div>
  );

  // === FLAT VARIANT ===
  if (variant === "flat") {
    return (
      <div className="w-full">
        {label && (
          <div className="flex items-center gap-2 mb-2">
            <IconPlaceholder
              lucide="Folder"
              hugeicons="FolderIcon"
              size={14}
              className="text-muted-foreground"
            />
            <span className="font-medium text-sm text-foreground">{label as string}</span>
          </div>
        )}
        <div className="pl-1">{renderDropZone()}</div>
      </div>
    );
  }

  // === GHOST VARIANT ===
  if (variant === "ghost") {
    return (
      <div className="w-full border border-border/50 rounded-lg p-4">
        {label && (
          <div className="flex items-center gap-2 mb-3">
            <IconPlaceholder
              lucide="Folder"
              hugeicons="FolderIcon"
              size={14}
              className="text-muted-foreground"
            />
            <span className="font-medium text-sm text-foreground">{label as string}</span>
          </div>
        )}
        {renderDropZone()}
      </div>
    );
  }

  // === BORDERED VARIANT ===
  if (variant === "bordered") {
    return (
      <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
        <div className="w-full border border-dashed border-border rounded-lg overflow-hidden">
          {label && (
            <CollapsibleTrigger className="w-full px-4 py-2 flex flex-row items-center justify-between hover:bg-muted/50 transition-colors select-none cursor-pointer">
              <div className="flex items-center gap-2">
                <IconPlaceholder
                  lucide="Folder"
                  hugeicons="FolderIcon"
                  size={14}
                  className="text-muted-foreground"
                />
                <span className="font-medium text-muted-foreground text-sm">
                  {label as string}
                </span>
              </div>
              <IconPlaceholder
                lucide="ChevronDown"
                hugeicons="ArrowDown01Icon"
                size={14}
                className={cn(
                  "text-muted-foreground transition-transform duration-200",
                  isCollapsed && "-rotate-90",
                )}
              />
            </CollapsibleTrigger>
          )}
          <CollapsibleContent>
            <div className="px-4 pt-2 pb-4">{renderDropZone()}</div>
          </CollapsibleContent>
        </div>
      </Collapsible>
    );
  }

  // === CARD VARIANT (DEFAULT) ===
  return (
    <Collapsible open={!isCollapsed} onOpenChange={() => toggleCollapsed(id)}>
      <Card className="w-full py-0 gap-0">
        {label && (
          <CardHeader className="p-0 border-b-0">
            <CollapsibleTrigger
              className={cn(
                "w-full px-4 py-3 flex flex-row items-center justify-between",
                "hover:bg-muted/75 bg-muted/50 transition-colors select-none cursor-pointer",
                !isCollapsed && "border-b",
              )}
            >
              <div className="flex items-center gap-2">
                <IconPlaceholder
                  lucide="Folder"
                  hugeicons="FolderIcon"
                  size={16}
                  className="text-muted-foreground"
                />
                <span className="text-sm font-semibold">{label as string}</span>
              </div>
              <IconPlaceholder
                lucide="ChevronDown"
                hugeicons="ArrowDown01Icon"
                size={16}
                className={cn(
                  "text-muted-foreground transition-transform duration-200",
                  isCollapsed && "-rotate-90",
                )}
              />
            </CollapsibleTrigger>
          </CardHeader>
        )}
        <CollapsibleContent>
          <CardContent className="p-4">{renderDropZone()}</CardContent>
        </CollapsibleContent>
      </Card>
    </Collapsible>
  );
};
registry/shadcn/builder/layout/row-renderer.tsx
"use client";

import { useDroppable } from "@dnd-kit/core";
import { horizontalListSortingStrategy } from "@dnd-kit/sortable";
import { cn } from "@/lib/utils";
import { IconPlaceholder } from "@/components/icon-placeholder";
import {
  DEFAULT_SLOT,
  type BuilderNodeRendererProps,
} from "@buildnbuzz/form-builder-react";

/**
 * Visual renderer for 'row' layout fields.
 * Displays children in a horizontal grid.
 */
export const RowRenderer = ({
  id,
  renderSlot,
  childrenIds,
}: BuilderNodeRendererProps) => {
  const { setNodeRef, isOver } = useDroppable({
    id: `${id}-dropzone`,
    data: { type: "row", parentId: id, parentSlot: DEFAULT_SLOT },
  });

  const isEmpty = childrenIds.length === 0;

  return (
    <div className="w-full">
      {/* Header */}
      <div className="flex items-center gap-1.5 mb-2 text-muted-foreground">
        <IconPlaceholder lucide="Columns" hugeicons="InsertRowDownIcon" size={14} />
        <span className="text-xs font-medium">Row</span>
      </div>

      {/* Drop zone container */}
      <div
        ref={setNodeRef}
        data-container-padding
        className={cn(
          "flex flex-row gap-3 p-3 min-h-16 rounded-lg border-2 border-dashed transition-colors",
          isEmpty ? "items-center justify-center" : "items-stretch",
          isOver
            ? "border-primary/50 bg-primary/5"
            : "border-muted-foreground/20 bg-muted/30 hover:border-muted-foreground/30",
        )}
      >
        {isEmpty ? (
          <div className="text-muted-foreground text-sm italic">
            Drop fields here
          </div>
        ) : (
          renderSlot(DEFAULT_SLOT, horizontalListSortingStrategy)
        )}
      </div>
    </div>
  );
};
registry/shadcn/builder/layout/tabs-renderer.tsx
"use client";

import React, { useMemo, useEffect } from "react";
import {
  useBuilderStore,
  type BuilderNodeRendererProps,
} from "@buildnbuzz/form-builder-react";
import { getTabSlotKeys } from "@buildnbuzz/form-builder-core";
import { useDroppable } from "@dnd-kit/core";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { cn } from "@/lib/utils";
import type { TabsField, Tab } from "@buildnbuzz/form-core";

function getTabDisplayLabel(tab: Tab, index: number) {
  const label = tab.label;
  if (typeof label === "string" && label.trim().length > 0) {
    return label;
  }
  if (typeof label === "number") {
    return String(label);
  }
  return `Tab ${index + 1}`;
}

interface TabDropZoneProps {
  containerId: string;
  slot: string;
  tab: Tab;
  childrenIds: string[];
  renderSlot: (slot: string) => React.ReactNode;
}

function TabDropZone({
  containerId,
  slot,
  tab,
  childrenIds,
  renderSlot,
}: TabDropZoneProps) {
  const { setNodeRef, isOver } = useDroppable({
    id: `${containerId}::tab::${encodeURIComponent(slot)}-dropzone`,
    data: { type: "tabs", parentId: containerId, parentSlot: slot },
  });

  const isEmpty = childrenIds.length === 0;
  const description = (tab as unknown as Record<string, unknown>).description;

  return (
    <div
      ref={setNodeRef}
      data-container-padding
      className={cn("min-h-16 transition-colors", isOver && "bg-primary/5")}
    >
      {typeof description === "string" && description && (
        <p className="text-sm text-muted-foreground mb-3">{description}</p>
      )}

      {isEmpty ? (
        <div
          className={cn(
            "h-16 flex items-center justify-center rounded-lg border-2 border-dashed transition-colors",
            isOver
              ? "border-primary/50 bg-primary/5"
              : "border-muted-foreground/20 bg-muted/30",
          )}
        >
          <span className="text-muted-foreground text-sm italic">
            Drop fields in this tab
          </span>
        </div>
      ) : (
        renderSlot(slot)
      )}
    </div>
  );
}

/**
 * Visual renderer for 'tabs' fields.
 * Orchestrates multiple slots as tabs.
 */
export const TabsRenderer = ({
  id,
  field,
  renderSlot,
}: BuilderNodeRendererProps) => {
  const tabsField = field as TabsField;
  const tabs = useMemo(() => tabsField.tabs ?? [], [tabsField.tabs]);
  const slots = useMemo(() => getTabSlotKeys(tabs), [tabs]);

  const activeSlotFromStore = useBuilderStore(
    (state) => state.activeTabs[id] ?? null,
  );
  const setActiveTab = useBuilderStore((state) => state.setActiveTab);
  const tabChildren = useBuilderStore(
    (state) => state.nodes[id]?.children ?? {},
  );

  const defaultSlot = useMemo(() => {
    if (slots.length === 0) return "";
    const configuredDefault = tabsField.ui?.defaultTab;
    const enabledSlots = slots.filter(
      (_, index) => tabs[index]?.disabled !== true,
    );
    const enabledFallbackSlot = enabledSlots[0] ?? slots[0];

    if (typeof configuredDefault === "number") {
      const index = Math.max(0, Math.min(slots.length - 1, configuredDefault));
      return tabs[index]?.disabled === true
        ? enabledFallbackSlot
        : slots[index];
    }

    if (typeof configuredDefault === "string") {
      const index = tabs.findIndex((tab) => tab.name === configuredDefault);
      if (index >= 0 && tabs[index]?.disabled !== true) {
        return slots[index];
      }
    }

    return enabledFallbackSlot;
  }, [tabsField.ui?.defaultTab, tabs, slots]);

  const activeSlot =
    activeSlotFromStore && slots.includes(activeSlotFromStore)
      ? activeSlotFromStore
      : defaultSlot;

  useEffect(() => {
    if (!activeSlot) return;
    if (activeSlotFromStore !== activeSlot) {
      setActiveTab(id, activeSlot);
    }
  }, [activeSlot, activeSlotFromStore, id, setActiveTab]);

  if (tabs.length === 0) {
    return (
      <div className="w-full rounded-lg border border-dashed border-muted-foreground/30 bg-muted/20 p-4">
        <div className="flex items-center gap-2 text-muted-foreground mb-2">
          <IconPlaceholder lucide="Layout" hugeicons="Layout01Icon" size={14} />
          <span className="text-xs font-medium">Tabs</span>
        </div>
        <p className="text-sm text-muted-foreground">
          Add at least one tab in the properties panel.
        </p>
      </div>
    );
  }

  return (
    <div className="w-full">
      <div className="flex items-center gap-1.5 mb-2 text-muted-foreground">
        <IconPlaceholder lucide="Layout" hugeicons="Layout01Icon" size={14} />
        <span className="text-xs font-medium">Tabs</span>
      </div>

      <Tabs
        value={activeSlot}
        onValueChange={(val) => val && setActiveTab(id, val)}
        className="w-full"
      >
        <TabsList
          variant={(tabsField.ui?.variant as "line" | "default") ?? "line"}
          className="w-full justify-start"
        >
          {tabs.map((tab, index) => {
            const slot = slots[index];
            const childCount = tabChildren[slot]?.length ?? 0;

            return (
              <TabsTrigger
                key={slot}
                value={slot}
                className={cn(
                  tab.disabled &&
                    "opacity-80 text-muted-foreground data-[state=active]:text-muted-foreground",
                )}
              >
                <span className="inline-flex items-center gap-1.5">
                  <span>{getTabDisplayLabel(tab, index)}</span>
                  <Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
                    {childCount}
                  </Badge>
                  {tab.disabled && (
                    <IconPlaceholder
                      lucide="Ban"
                      hugeicons="BlockedIcon"
                      size={12}
                      className="text-muted-foreground/70"
                    />
                  )}
                </span>
              </TabsTrigger>
            );
          })}
        </TabsList>

        {tabs.map((tab, index) => {
          const slot = slots[index];
          const childrenIdsForSlot = tabChildren[slot] || [];

          return (
            <TabsContent key={slot} value={slot} className="mt-4">
              {tab.name && (
                <p className="text-xs text-muted-foreground mb-2">
                  Key: <code className="font-mono">{tab.name}</code>
                  {tab.disabled && (
                    <Badge
                      variant="outline"
                      className="ml-2 h-5 gap-1 border-muted-foreground/20 bg-muted/50 px-1.5 text-[10px] font-normal text-muted-foreground"
                    >
                      <IconPlaceholder
                        lucide="Ban"
                        hugeicons="BlockedIcon"
                        size={12}
                      />
                      Disabled
                    </Badge>
                  )}
                </p>
              )}
              <TabDropZone
                containerId={id}
                slot={slot}
                tab={tab}
                childrenIds={childrenIdsForSlot}
                renderSlot={renderSlot}
              />
            </TabsContent>
          );
        })}
      </Tabs>
    </div>
  );
};
registry/shadcn/builder/mobile-overlay.tsx
"use client";

import { useIsMobile } from "@/hooks/use-mobile";
import { IconPlaceholder } from "@/components/icon-placeholder";

/**
 * Overlay shown on mobile devices to prompt users to use a desktop browser.
 */
export function MobileOverlay() {
  const isMobile = useIsMobile();

  if (!isMobile) return null;

  return (
    <div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 p-6 backdrop-blur-md lg:hidden">
      <div className="relative w-full max-w-sm overflow-hidden rounded-3xl border border-border/60 bg-card/90 shadow-2xl shadow-black/20">
        <div className="absolute -left-16 -top-16 h-44 w-44 rounded-full bg-primary/20 blur-3xl" />
        <div className="absolute -bottom-20 -right-10 h-48 w-48 rounded-full bg-sky-500/15 blur-3xl" />

        <div className="relative space-y-4 p-8 text-center">
          <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl border border-border/60 bg-background/70 shadow-sm">
            <IconPlaceholder
              lucide="Monitor"
              hugeicons="ComputerIcon"
              tabler="IconDeviceDesktop"
              phosphor="Monitor"
              remixicon="RiComputerLine"
              size={24}
              className="text-primary"
            />
          </div>
          <div className="space-y-2">
            <h1 className="text-xl font-bold tracking-tight">
              Best on Desktop
            </h1>
            <p className="text-sm text-muted-foreground leading-relaxed">
              BuzzForm Builder requires a larger screen to provide the best
              editing experience. Please switch to a desktop or tablet.
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}
registry/shadcn/builder/node-wrapper.tsx
"use client";

import React, { useState } from "react";
import { useBuilderStore, SortableNode } from "@buildnbuzz/form-builder-react";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { Node } from "@buildnbuzz/form-builder-core";
import { isDataField, isContainerType } from "@buildnbuzz/form-core";

interface NodeWrapperProps {
  node: Node;
  children: React.ReactNode;
}

/**
 * Visual frame for canvas nodes.
 * Provides selection states, drag handling, and a contextual toolbar.
 */
export const NodeWrapper = ({ node, children }: NodeWrapperProps) => {
  const selectedId = useBuilderStore((s) => s.selectedId);
  const selectNode = useBuilderStore((s) => s.selectNode);
  const removeNode = useBuilderStore((s) => s.removeNode);
  const duplicateNode = useBuilderStore((s) => s.duplicateNode);

  const [isHovered, setIsHovered] = useState(false);
  const isSelected = selectedId === node.id;
  const isHidden = isDataField(node.field) && node.field.hidden === true;

  const fieldStyle = "style" in node.field ? node.field.style : undefined;
  const fieldWidth = (fieldStyle as React.CSSProperties | undefined)?.width;

  return (
    <SortableNode
      id={node.id}
      render={({ setNodeRef, attributes, listeners, style, isDragging }) => (
        <div
          ref={setNodeRef}
          style={{
            ...style,
            ...(isHidden && {
              backgroundImage:
                "repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(128,128,128,0.05) 10px, rgba(128,128,128,0.05) 20px)",
            }),
            ...(fieldWidth && fieldWidth !== "auto"
              ? { width: fieldWidth, flex: "0 0 auto" }
              : { flex: "1 1 0%", minWidth: 0 }),
          }}
          onClick={(e) => {
            e.stopPropagation();
            selectNode(node.id);
          }}
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
          {...attributes}
          {...listeners}
          className={cn(
            "relative group/node my-2 rounded-xl transition-all duration-200 cursor-default",
            "hover:p-2",
            isSelected
              ? "border-primary/50 ring-2 ring-primary/10 p-2 bg-primary/5"
              : "border-transparent",
            !isSelected && "border hover:border-border/40",
            isDragging && "opacity-30",
            isHidden &&
              "opacity-60 grayscale border-dashed border-muted-foreground/30",
          )}
        >
          {/* Node Toolbar - visible on hover or selection */}
          <TooltipProvider>
            <div
              className={cn(
                "absolute -top-9 right-0 z-50 flex items-center gap-0.5 rounded-lg border border-border/50 bg-card/75 p-1 shadow-lg shadow-black/10 backdrop-blur-md transition-opacity duration-200",
                isSelected || isHovered
                  ? "opacity-100 pointer-events-auto"
                  : "opacity-0 pointer-events-none",
              )}
              onClick={(e) => e.stopPropagation()}
              onPointerDown={(e) => e.stopPropagation()}
            >
              <Tooltip>
                <TooltipTrigger
                  render={
                    <Button
                      variant="ghost"
                      size="icon"
                      className="size-7 text-muted-foreground hover:text-primary"
                      onClick={(e) => {
                        e.stopPropagation();
                        duplicateNode(node.id);
                      }}
                    />
                  }
                >
                  <IconPlaceholder
                    lucide="Copy"
                    hugeicons="Copy01Icon"
                    tabler="IconCopy"
                    phosphor="Copy"
                    remixicon="RiFileCopyLine"
                    size={14}
                  />
                </TooltipTrigger>
                <TooltipContent>Duplicate field</TooltipContent>
              </Tooltip>

              <Tooltip>
                <TooltipTrigger
                  render={
                    <Button
                      variant="ghost"
                      size="icon"
                      className="size-7 text-muted-foreground hover:text-destructive"
                      onClick={(e) => {
                        e.stopPropagation();
                        removeNode(node.id);
                      }}
                    />
                  }
                >
                  <IconPlaceholder
                    lucide="Trash2"
                    hugeicons="Delete02Icon"
                    tabler="IconTrash"
                    phosphor="Trash"
                    remixicon="RiDeleteBinLine"
                    size={14}
                  />
                </TooltipTrigger>
                <TooltipContent>Remove field</TooltipContent>
              </Tooltip>
            </div>
          </TooltipProvider>

          {/* Node Content */}
          <div
            className={cn(
              "relative w-full transition-all duration-200",
              !isContainerType(node.field.type) && "pointer-events-none",
            )}
          >
            {isHidden && (
              <Badge
                className="absolute bottom-1.5 right-2 z-10 select-none bg-background/80 backdrop-blur-sm pointer-events-auto"
                variant="outline"
              >
                Hidden
              </Badge>
            )}
            {children}
          </div>
        </div>
      )}
    />
  );
};
registry/shadcn/builder/preview/preview-form.tsx
"use client";

import { useBuilderFormContext } from "@buildnbuzz/form-builder-react";
import {
  Form,
  FormContent,
  FormFields,
  FormActions,
  FormSubmit,
} from "@/registry/shadcn/form";

/**
 * PreviewForm: Renders the builder's current state as a functional shadcn-based form.
 * Uses the form instance provided by BuilderFormProvider.
 */
export function PreviewForm() {
  const { form, fields } = useBuilderFormContext();

  return (
    <Form form={form} schema={{ fields }}>
      <FormContent noValidate className="p-0 gap-6">
        <FormFields />
        <FormActions className="mt-4" align="start">
          <FormSubmit>Submit</FormSubmit>
        </FormActions>
      </FormContent>
    </Form>
  );
}
registry/shadcn/builder/property-panel.tsx
"use client";

import React from "react";
import {
  useBuilderContext,
  BuilderProperties,
  useSyncProperty,
  getRegistryEntry,
} from "@buildnbuzz/form-builder-react";
import { Form, FormContent, useFormContext } from "@/registry/shadcn/form";
import { ScrollArea } from "@/components/ui/scroll-area";
import { FieldIcon } from "./field-icon";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { flattenFieldToFormValues } from "@buildnbuzz/form-builder-core";
import type { Field, FormSchema } from "@buildnbuzz/form-core";
import { ExpressionCheckbox } from "./expression-builder/expression-checkbox";
import { registry as shadcnRegistry } from "../registry";

/** Renders nothing — syncs form state ↔ builder store bidirectionally. */
function SyncPropertyState({
  update,
  data,
  schema,
}: {
  update: (data: Record<string, unknown>) => void;
  data: Record<string, unknown>;
  schema: Field[];
}) {
  const { form } = useFormContext();
  useSyncProperty({ form, data, schema, update });
  return null;
}

/**
 * Side panel for editing node or global form properties.
 */
export const PropertyPanel = () => {
  const { registry } = useBuilderContext();

  return (
    <div className="flex h-full flex-col bg-background border-l overflow-hidden">
      <BuilderProperties
        render={({ node, id, schema, data, update }) => {
          const entry = node
            ? getRegistryEntry(registry, node.field.type)
            : null;
          const label =
            entry?.sidebar.label ??
            (id === "form" ? "Form Settings" : "Properties");

          const defaultValues = flattenFieldToFormValues(
            data as unknown as Field,
            schema,
          );

          return (
            <>
              <div className="p-4 border-b h-header flex items-center">
                {node && entry ? (
                  <div className="flex items-center gap-3 w-full">
                    <div className="flex h-10 w-10 items-center justify-center rounded-xl border bg-secondary/30 shadow-sm">
                      {node && (
                        <FieldIcon
                          type={node.field.type}
                          size={20}
                          className="text-foreground"
                        />
                      )}
                    </div>
                    <div className="flex flex-col gap-0.5 min-w-0 flex-1">
                      <span className="font-semibold text-sm truncate">
                        {label}
                      </span>
                      <span className="text-[11px] text-muted-foreground/80 truncate">
                        Properties
                      </span>
                    </div>
                  </div>
                ) : (
                  <div className="flex items-center gap-3 w-full">
                    <div className="flex h-10 w-10 items-center justify-center rounded-xl border bg-secondary/30 shadow-sm">
                      <IconPlaceholder
                        lucide="Settings"
                        hugeicons="Settings01Icon"
                        size={20}
                        className="text-muted-foreground"
                      />
                    </div>
                    <div className="flex flex-col gap-0.5 min-w-0 flex-1">
                      <span className="font-semibold text-sm">
                        Form Settings
                      </span>
                      <span className="text-[11px] text-muted-foreground/80 truncate">
                        Configuration
                      </span>
                    </div>
                  </div>
                )}
              </div>
              <div className="flex-1 min-h-0">
                <ScrollArea className="h-full">
                  <div className="flex flex-col gap-6 p-4">
                    {schema.length > 0 ? (
                      <Form
                        key={id}
                        schema={{ fields: schema } as unknown as FormSchema}
                        defaultValues={defaultValues}
                        registries={{
                          fields: {
                            ...shadcnRegistry,
                            checkbox: ExpressionCheckbox,
                            switch: ExpressionCheckbox,
                          },
                        }}
                      >
                        <FormContent autoRender as="div" className="gap-6">
                          <SyncPropertyState
                            update={update}
                            data={data}
                            schema={schema}
                          />
                        </FormContent>
                      </Form>
                    ) : (
                      <div className="flex flex-col items-center justify-center gap-2 py-10 text-center opacity-30">
                        <span className="text-[11px]">
                          No properties available
                        </span>
                      </div>
                    )}
                  </div>
                </ScrollArea>
              </div>
            </>
          );
        }}
      />
    </div>
  );
};
registry/shadcn/builder/registry.tsx
"use client";

import React from "react";
import {
  DEFAULT_FIELD_REGISTRY,
  isDataField,
} from "@buildnbuzz/form-builder-core";
import { Field, type DataField } from "@buildnbuzz/form-react";
import { registry as shadcnRegistry } from "../registry";
import { RowRenderer } from "./layout/row-renderer";
import { GroupRenderer } from "./layout/group-renderer";
import { ArrayRenderer } from "./layout/array-renderer";
import { TabsRenderer } from "./layout/tabs-renderer";
import { CollapsibleRenderer } from "./layout/collapsible-renderer";
import {
  useBuilderFormContext,
  type BuilderNodeRendererProps,
  type BuilderFieldRegistry,
} from "@buildnbuzz/form-builder-react";

/**
 * Generic renderer for data fields that uses the runtime components.
 * Wraps the component in a Field to provide the necessary headless context.
 */
const DataFieldRenderer = ({ field }: BuilderNodeRendererProps) => {
  const { form } = useBuilderFormContext();
  const Component = shadcnRegistry[field.type];

  if (!Component || !isDataField(field)) {
    return null;
  }

  return (
    <Field field={field as DataField} form={form}>
      <Component />
    </Field>
  );
};

const specializedRenderers: Record<
  string,
  React.ComponentType<BuilderNodeRendererProps>
> = {
  row: RowRenderer,
  group: GroupRenderer,
  array: ArrayRenderer,
  tabs: TabsRenderer,
  collapsible: CollapsibleRenderer,
};

/**
 * Shadcn-specific builder registry.
 * Hydrates the core metadata registry with React renderers.
 */
export const SHADCN_BUILDER_REGISTRY: BuilderFieldRegistry = Object.entries(
  DEFAULT_FIELD_REGISTRY,
).reduce((acc, [type, entry]) => {
  if (!entry) return acc;

  acc[type] = {
    ...entry,
    renderer: (specializedRenderers[type] ??
      DataFieldRenderer) as React.ComponentType<BuilderNodeRendererProps>,
  };
  return acc;
}, {} as BuilderFieldRegistry);

More from buzzform

Browse all components in this library →