- {/* Form Header */}
-
-
- updateFormConfig({ title: e.target.value })
- }
- placeholder="Untitled Form"
- className="w-full bg-transparent text-2xl md:text-3xl font-semibold placeholder:text-muted-foreground/50 focus:outline-none border-none p-0 mb-2"
- />
-
- updateFormConfig({ description: e.target.value })
- }
- placeholder="Add a description..."
- className="w-full bg-transparent text-muted-foreground placeholder:text-muted-foreground/50 focus:outline-none border-none p-0"
- />
+
+
+
+
+
+ {viewMode === "builder" && }
+
+
+
+ {viewMode === "builder" && (
+
+ {/* Form Header */}
+
+
+ updateFormConfig({ title: e.target.value })
+ }
+ placeholder="Untitled Form"
+ className="w-full bg-transparent text-2xl md:text-3xl font-semibold placeholder:text-muted-foreground/50 focus:outline-none border-none p-0 mb-2"
+ />
+
+ updateFormConfig({ description: e.target.value })
+ }
+ placeholder="Add a description..."
+ className="w-full bg-transparent text-muted-foreground placeholder:text-muted-foreground/50 focus:outline-none border-none p-0"
+ />
+
+
+ {/* Fields */}
+
+ {formConfig.fields.length === 0 ? (
+
+
+
+ No fields yet
+
+
+ Drag a field from the sidebar or click to add
+
+
+ ) : (
+ f.id)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {formConfig.fields.map((field) => (
+ setSelectedFieldId(field.id)}
+ onDelete={() => deleteField(field.id)}
+ onDuplicate={() => duplicateField(field.id)}
+ onUpdate={updateField}
+ />
+ ))}
+
+
+ )}
+
+ )}
- {/* Fields */}
- {formConfig.fields.length === 0 ? (
-
-
-
- No fields yet
-
-
- Click a field type in the sidebar to add it
-
+ {viewMode === "preview" && (
+
+
+
+ )}
+
+ {viewMode === "json" && (
+
+
+
+ {JSON.stringify(formConfig, null, 2)}
+
- ) : (
-
{
+ navigator.clipboard.writeText(
+ JSON.stringify(formConfig, null, 2)
+ );
+ toast.success("Copied to clipboard");
+ }}
+ variant="outline"
>
- f.id)}
- strategy={verticalListSortingStrategy}
- >
-
- {formConfig.fields.map((field) => (
- setSelectedFieldId(field.id)}
- onDelete={() => deleteField(field.id)}
- onDuplicate={() => duplicateField(field.id)}
- onUpdate={updateField}
- />
- ))}
-
-
-
- )}
-
- )}
-
- {viewMode === "preview" && (
-
-
-
- )}
-
- {viewMode === "json" && (
-
-
-
- {JSON.stringify(formConfig, null, 2)}
-
+ Copy to Clipboard
+
-
-
- )}
-
-
-
+ )}
+
+
+
+
+ {activeId && isToolboxItem(activeId) && (
+
+
+
+ )}
+
);
}
diff --git a/registry/form-builder/lib/dnd-utils.ts b/registry/form-builder/lib/dnd-utils.ts
new file mode 100644
index 0000000..552c6d5
--- /dev/null
+++ b/registry/form-builder/lib/dnd-utils.ts
@@ -0,0 +1,16 @@
+import type { FieldType } from "./form-config";
+
+export const TOOLBOX_PREFIX = "toolbox-";
+export const CANVAS_DROP_ZONE_ID = "canvas-drop-zone";
+
+export function isToolboxItem(id: string | number): boolean {
+ return typeof id === "string" && id.startsWith(TOOLBOX_PREFIX);
+}
+
+export function getFieldTypeFromToolboxId(id: string): FieldType {
+ return id.replace(TOOLBOX_PREFIX, "") as FieldType;
+}
+
+export function createToolboxId(fieldType: FieldType): string {
+ return `${TOOLBOX_PREFIX}${fieldType}`;
+}
diff --git a/registry/form-builder/toolbox-drag-overlay.tsx b/registry/form-builder/toolbox-drag-overlay.tsx
new file mode 100644
index 0000000..71a503c
--- /dev/null
+++ b/registry/form-builder/toolbox-drag-overlay.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { FIELD_TYPES, FieldType } from "./lib/form-config";
+
+interface ToolboxDragOverlayProps {
+ fieldType: FieldType;
+}
+
+export function ToolboxDragOverlay({ fieldType }: ToolboxDragOverlayProps) {
+ const fieldConfig = FIELD_TYPES.find((f) => f.type === fieldType)!;
+ const Icon = fieldConfig.icon;
+
+ return (
+
+
+ {fieldConfig.label}
+
+ );
+}
diff --git a/registry/form-builder/use-form-builder.ts b/registry/form-builder/use-form-builder.ts
index aee54b9..1f6459e 100644
--- a/registry/form-builder/use-form-builder.ts
+++ b/registry/form-builder/use-form-builder.ts
@@ -7,6 +7,11 @@ import { toast } from "sonner";
import type { FormConfig, FieldType, FieldConfig } from "./lib/form-config";
import { createDefaultField, createEmptyForm } from "./lib/form-config";
import { downloadFormConfig, parseFormConfig } from "./lib/form-utils";
+import {
+ isToolboxItem,
+ getFieldTypeFromToolboxId,
+ CANVAS_DROP_ZONE_ID,
+} from "./lib/dnd-utils";
export type ViewMode = "builder" | "preview" | "json";
@@ -24,6 +29,19 @@ export function useFormBuilder() {
});
};
+ const addFieldAtIndex = (type: FieldType, index: number) => {
+ const newField = createDefaultField(type, formConfig.fields.length);
+ setFormConfig((prev) => {
+ const fields = [...prev.fields];
+ fields.splice(index, 0, newField);
+ return { ...prev, fields };
+ });
+ setSelectedFieldId(newField.id);
+ toast.success("Field added", {
+ description: `${type} field added to the form`,
+ });
+ };
+
const deleteField = (fieldId: string) => {
setFormConfig((prev) => ({
...prev,
@@ -64,13 +82,41 @@ export function useFormBuilder() {
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
- if (active.id === over?.id) return;
- setFormConfig((prev) => {
- const oldIndex = prev.fields.findIndex((f) => f.id === active.id);
- const newIndex = prev.fields.findIndex((f) => f.id === over?.id);
- return { ...prev, fields: arrayMove(prev.fields, oldIndex, newIndex) };
- });
+ if (!over) return;
+
+ const activeId = active.id as string;
+ const overId = over.id as string;
+
+ if (isToolboxItem(activeId)) {
+ const fieldType = getFieldTypeFromToolboxId(activeId);
+
+ if (overId === CANVAS_DROP_ZONE_ID) {
+ addField(fieldType);
+ return;
+ }
+
+ const overIndex = formConfig.fields.findIndex((f) => f.id === overId);
+ if (overIndex !== -1) {
+ addFieldAtIndex(fieldType, overIndex);
+ return;
+ }
+
+ addField(fieldType);
+ return;
+ }
+
+ if (activeId !== overId) {
+ const oldIndex = formConfig.fields.findIndex((f) => f.id === activeId);
+ const newIndex = formConfig.fields.findIndex((f) => f.id === overId);
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ setFormConfig((prev) => ({
+ ...prev,
+ fields: arrayMove(prev.fields, oldIndex, newIndex),
+ }));
+ }
+ }
};
const handleExport = () => downloadFormConfig(formConfig);