# Form Builder

> A reusable form builder block with drag-and-drop fields, JSON definitions, previews, and keyed response data.

A shadcn/ui block from the [@corr](https://ui.corr.sh/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/corr/form-builder).

- Page, with a live preview: https://shadcn.exchange/r/corr/form-builder
- Screenshot: https://shadcn.exchange/shot/corr/form-builder/light.png
- Registry item: https://ui.corr.sh/r/form-builder.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/corr/form-builder.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@dnd-kit/core`, `date-fns`, `lucide-react`, `motion`, `react-day-picker`
- Registry items: `badge`, `button`, `calendar`, `card`, `checkbox`, `combobox`, `dialog`, `drawer`, `input`, `label`, `popover`, `scroll-area`, `select`, `https://ui.corr.sh/r/screen-size-guard.json`, `https://ui.corr.sh/r/stepper.json`, `tabs`, `textarea`

## Files

### `registry/corr/components/form-builder.tsx`

```tsx
"use client"

import * as React from "react"
import { createPortal } from "react-dom"
import {
  CalendarIcon,
  EyeIcon,
  FileTextIcon,
  GripVerticalIcon,
  HashIcon,
  LayoutPanelTopIcon,
  ListChecksIcon,
  MailIcon,
  PanelRightIcon,
  Rows3Icon,
  SplitSquareHorizontalIcon,
  TextCursorInputIcon,
  ToggleLeftIcon,
  Trash2Icon,
} from "lucide-react"
import {
  DndContext,
  DragOverlay,
  PointerSensor,
  pointerWithin,
  rectIntersection,
  useDraggable,
  useDroppable,
  useSensor,
  useSensors,
  type CollisionDetection,
  type DragEndEvent,
  type DragStartEvent,
  type Modifier,
} from "@dnd-kit/core"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

import { DatePickerField } from "./date-picker-field"
import { Stepper } from "./stepper"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
  useComboboxAnchor,
} from "@/components/ui/combobox"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
} from "@/components/ui/drawer"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"

export type FormFieldType =
  | "text"
  | "textarea"
  | "number"
  | "email"
  | "date"
  | "datetime"
  | "select"
  | "multi-select"
  | "checkbox"
  | "checkbox-group"
  | "static-text"
  | "page-break"

export type FormVisibilityOperator =
  | "equals"
  | "not-equals"
  | "contains"
  | "not-contains"
  | "is-empty"
  | "is-not-empty"

export type FormVisibilityRule = {
  targetField: string
  dependsOnField: string
  operator: FormVisibilityOperator
  value?: unknown
}

export type FormField = {
  id: string
  key: string
  label: string
  type: FormFieldType
  required?: boolean
  helpText?: string
  placeholder?: string
  options?: string[]
  defaultValue?: unknown
  layout?: { row: number; col: number }
  stepLabel?: string
}

export type FormActor = {
  id: string
  name: string
  email?: string
}

export type FormDefinitionStatus = "draft" | "published" | "archived"

export type FormDefinitionMetadata = {
  author?: FormActor
  createdAt?: string
  modifiedAt?: string
  modifiedBy?: FormActor
  publishedAt?: string
  status?: FormDefinitionStatus
}

export type FormDefinition = {
  id?: string
  version?: number
  title?: string
  description?: string
  metadata?: FormDefinitionMetadata
  fields: FormField[]
  visibilityRules?: FormVisibilityRule[]
}

export type FormResponses = Record<string, unknown>

export type FormResponseStatus = "draft" | "submitted" | "archived"

export type FormResponseMetadata = {
  createdAt: string
  createdBy?: FormActor
  modifiedAt: string
  modifiedBy?: FormActor
  submittedAt?: string
  submittedBy?: FormActor
  status: FormResponseStatus
}

export type FormResponseRecord = {
  id: string
  formId: string
  formVersion: number
  definitionSnapshot: FormDefinition
  responses: FormResponses
  metadata: FormResponseMetadata
}

export type FormResponseRenderMode = "submitted-version" | "latest-version"

export type FormPage = {
  id: string
  label: string
  fields: FormField[]
}

const fieldTypes: Array<{
  type: FormFieldType
  label: string
  icon: React.ComponentType<{ className?: string }>
}> = [
  { type: "text", label: "Text", icon: TextCursorInputIcon },
  { type: "textarea", label: "Long text", icon: Rows3Icon },
  { type: "number", label: "Number", icon: HashIcon },
  { type: "email", label: "Email", icon: MailIcon },
  { type: "date", label: "Date", icon: CalendarIcon },
  { type: "datetime", label: "Date time", icon: CalendarIcon },
  { type: "select", label: "Select", icon: SplitSquareHorizontalIcon },
  { type: "multi-select", label: "Multi-select", icon: ListChecksIcon },
  { type: "checkbox", label: "Checkbox", icon: ToggleLeftIcon },
  { type: "checkbox-group", label: "Checkbox group", icon: ListChecksIcon },
  { type: "static-text", label: "Text block", icon: FileTextIcon },
  { type: "page-break", label: "Page break", icon: LayoutPanelTopIcon },
]

const visibilityOperators: Array<{
  value: FormVisibilityOperator
  label: string
}> = [
  { value: "equals", label: "Equals" },
  { value: "not-equals", label: "Does not equal" },
  { value: "contains", label: "Contains" },
  { value: "not-contains", label: "Does not contain" },
  { value: "is-empty", label: "Is empty" },
  { value: "is-not-empty", label: "Is not empty" },
]

function createId(prefix = "field") {
  return `${prefix}-${Math.random().toString(36).slice(2, 9)}`
}

function toFieldKey(value: string) {
  return value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
}

function uniqueFieldKey(definition: FormDefinition, type: FormFieldType) {
  const base = type.replace(/-/g, "_")
  let index = definition.fields.length + 1
  while (definition.fields.some((field) => field.key === `${base}_${index}`)) {
    index += 1
  }
  return `${base}_${index}`
}

export function createFormField(
  type: FormFieldType,
  definition: FormDefinition = { fields: [] }
): FormField {
  const typeMeta = fieldTypes.find((item) => item.type === type)
  const label = typeMeta?.label ?? "Field"
  const key = uniqueFieldKey(definition, type)

  return {
    id: createId(),
    key,
    label,
    type,
    required: false,
    placeholder:
      type === "text" || type === "email" || type === "number"
        ? `Enter ${label.toLowerCase()}`
        : undefined,
    options:
      type === "select" ||
      type === "multi-select" ||
      type === "checkbox-group"
        ? ["Option 1", "Option 2"]
        : undefined,
    defaultValue:
      type === "static-text"
        ? "Add supporting instructions or section context."
        : undefined,
    stepLabel: type === "page-break" ? "" : undefined,
  }
}

function orderedFields(fields: FormField[]) {
  return [...fields]
    .map((field, index) => ({
      field,
      index,
      row: field.layout?.row ?? index,
      col: field.layout?.col ?? 0,
    }))
    .sort((left, right) => left.row - right.row || left.col - right.col || left.index - right.index)
    .map((item) => item.field)
}

function fieldRows(fields: FormField[]) {
  const rows: FormField[][] = []
  let currentRow: FormField[] = []

  function flushRow() {
    if (currentRow.length === 0) return
    rows.push(currentRow)
    currentRow = []
  }

  for (const field of orderedFields(fields)) {
    if (field.type === "page-break") {
      flushRow()
      rows.push([field])
      continue
    }

    currentRow.push(field)
    if (currentRow.length === 3) flushRow()
  }

  flushRow()
  return rows
}

export function normalizeFormDefinition(
  definition: FormDefinition
): FormDefinition {
  const ordered = orderedFields(definition.fields)
  const fields: FormField[] = []
  let hasDataInPage = false

  for (const field of ordered) {
    if (field.type !== "page-break") {
      fields.push(field)
      hasDataInPage = true
      continue
    }

    if (!hasDataInPage) continue
    fields.push({
      ...field,
      required: false,
      options: undefined,
      defaultValue: undefined,
    })
    hasDataInPage = false
  }

  while (fields.at(-1)?.type === "page-break") fields.pop()

  return {
    ...definition,
    fields: applyFieldLayout(fields),
    visibilityRules: definition.visibilityRules ?? [],
  }
}

function applyFieldLayout(fields: FormField[]) {
  const nextFields: FormField[] = []
  let row = 0
  let col = 0

  for (const field of fields) {
    if (field.type === "page-break") {
      if (col > 0) {
        row += 1
        col = 0
      }

      nextFields.push({ ...field, layout: { row, col: 0 } })
      row += 1
      col = 0
      continue
    }

    nextFields.push({ ...field, layout: { row, col } })
    col += 1

    if (col >= 3) {
      row += 1
      col = 0
    }
  }

  return nextFields
}

export function buildFormPages(definition: FormDefinition): FormPage[] {
  const normalized = normalizeFormDefinition(definition)
  const pages: FormPage[] = []
  let currentFields: FormField[] = []
  let nextLabel: string | undefined

  function pushPage() {
    if (currentFields.length === 0) return
    const index = pages.length + 1
    pages.push({
      id: `page-${index}`,
      label: nextLabel?.trim() || `Step ${index}`,
      fields: currentFields,
    })
    currentFields = []
    nextLabel = undefined
  }

  for (const field of orderedFields(normalized.fields)) {
    if (field.type === "page-break") {
      pushPage()
      nextLabel = field.stepLabel
      continue
    }
    currentFields.push(field)
  }

  pushPage()
  return pages
}

function matchesVisibilityRule(
  operator: FormVisibilityOperator,
  actualValue: unknown,
  expectedValue: unknown
): boolean {
  if (operator === "equals") return actualValue === expectedValue
  if (operator === "not-equals") return actualValue !== expectedValue
  if (operator === "contains") {
    if (Array.isArray(actualValue)) return actualValue.includes(expectedValue)
    return String(actualValue ?? "").includes(String(expectedValue ?? ""))
  }
  if (operator === "not-contains") {
    if (Array.isArray(actualValue)) return !actualValue.includes(expectedValue)
    return !String(actualValue ?? "").includes(String(expectedValue ?? ""))
  }
  if (operator === "is-empty") {
    return (
      actualValue === undefined ||
      actualValue === null ||
      actualValue === "" ||
      (Array.isArray(actualValue) && actualValue.length === 0)
    )
  }
  if (operator === "is-not-empty") {
    return !matchesVisibilityRule("is-empty", actualValue, expectedValue)
  }
  return true
}

function getFieldVisibility(
  definition: FormDefinition,
  responses: FormResponses
) {
  const visibility: Record<string, boolean> = {}
  for (const field of definition.fields) visibility[field.key] = true
  for (const rule of definition.visibilityRules ?? []) {
    visibility[rule.targetField] = matchesVisibilityRule(
      rule.operator,
      responses[rule.dependsOnField],
      rule.value
    )
  }
  return visibility
}

export function validateFormResponses(
  definition: FormDefinition,
  responses: FormResponses,
  scopedFieldKeys?: string[]
) {
  const errors: Record<string, string> = {}
  const visibility = getFieldVisibility(definition, responses)
  const scope = scopedFieldKeys ? new Set(scopedFieldKeys) : null

  for (const field of definition.fields) {
    if (scope && !scope.has(field.key)) continue
    if (!visibility[field.key]) continue
    if (field.type === "static-text" || field.type === "page-break") continue
    if (!field.required) continue

    const value = responses[field.key]
    const isEmpty =
      value === undefined ||
      value === null ||
      value === "" ||
      (Array.isArray(value) && value.length === 0)

    if (isEmpty) errors[field.key] = `${field.label} is required`
  }

  return errors
}

function initialResponses(definition: FormDefinition): FormResponses {
  const responses: FormResponses = {}

  for (const field of definition.fields) {
    if (field.type === "static-text" || field.type === "page-break") continue
    if (field.defaultValue !== undefined && field.defaultValue !== null) {
      responses[field.key] = field.defaultValue
    } else if (field.type === "checkbox") {
      responses[field.key] = false
    } else if (field.type === "multi-select" || field.type === "checkbox-group") {
      responses[field.key] = []
    }
  }

  return responses
}

function nowIso() {
  return new Date().toISOString()
}

function formIdFor(definition: FormDefinition) {
  return definition.id ?? (toFieldKey(definition.title ?? "form") || "form")
}

function formVersionFor(definition: FormDefinition) {
  return definition.version ?? 1
}

export function createFormResponseRecord(
  definition: FormDefinition,
  responses: FormResponses,
  options: {
    id?: string
    actor?: FormActor
    createdAt?: string
    submittedAt?: string
    status?: FormResponseStatus
  } = {}
): FormResponseRecord {
  const timestamp = options.createdAt ?? nowIso()
  const status = options.status ?? "submitted"

  return {
    id: options.id ?? createId("response"),
    formId: formIdFor(definition),
    formVersion: formVersionFor(definition),
    definitionSnapshot: normalizeFormDefinition(definition),
    responses,
    metadata: {
      createdAt: timestamp,
      createdBy: options.actor,
      modifiedAt: timestamp,
      modifiedBy: options.actor,
      submittedAt: status === "submitted" ? options.submittedAt ?? timestamp : undefined,
      submittedBy: status === "submitted" ? options.actor : undefined,
      status,
    },
  }
}

export function updateFormResponseRecord(
  record: FormResponseRecord,
  responses: FormResponses,
  options: {
    actor?: FormActor
    modifiedAt?: string
    status?: FormResponseStatus
  } = {}
): FormResponseRecord {
  const timestamp = options.modifiedAt ?? nowIso()
  const status = options.status ?? record.metadata.status

  return {
    ...record,
    responses,
    metadata: {
      ...record.metadata,
      modifiedAt: timestamp,
      modifiedBy: options.actor ?? record.metadata.modifiedBy,
      submittedAt:
        status === "submitted"
          ? record.metadata.submittedAt ?? timestamp
          : record.metadata.submittedAt,
      submittedBy:
        status === "submitted"
          ? record.metadata.submittedBy ?? options.actor
          : record.metadata.submittedBy,
      status,
    },
  }
}

export function resolveFormDefinitionForResponse(
  record: FormResponseRecord,
  latestDefinition?: FormDefinition,
  mode: FormResponseRenderMode = "submitted-version"
) {
  if (mode === "latest-version" && latestDefinition) {
    return normalizeFormDefinition(latestDefinition)
  }

  return normalizeFormDefinition(record.definitionSnapshot)
}

export function getFormResponseValues(
  response: FormResponseRecord | FormResponses
): FormResponses {
  if (
    response &&
    typeof response === "object" &&
    "responses" in response &&
    typeof response.responses === "object" &&
    response.responses !== null
  ) {
    return response.responses as FormResponses
  }

  return response as FormResponses
}

function useIsSmallScreen() {
  const [isSmallScreen, setIsSmallScreen] = React.useState(false)

  React.useEffect(() => {
    const media = window.matchMedia("(max-width: 639px)")
    const sync = () => setIsSmallScreen(media.matches)
    sync()
    media.addEventListener("change", sync)

    return () => media.removeEventListener("change", sync)
  }, [])

  return isSmallScreen
}

function useMediaQuery(query: string) {
  const [matches, setMatches] = React.useState(false)

  React.useEffect(() => {
    const media = window.matchMedia(query)
    const sync = () => setMatches(media.matches)
    sync()
    media.addEventListener("change", sync)

    return () => media.removeEventListener("change", sync)
  }, [query])

  return matches
}

const snapOverlayToCursor: Modifier = ({
  activeNodeRect,
  activatorEvent,
  overlayNodeRect,
  transform,
}) => {
  if (!activeNodeRect || !overlayNodeRect || !activatorEvent) return transform
  if (!("clientX" in activatorEvent) || !("clientY" in activatorEvent)) {
    return transform
  }

  const { clientX, clientY } = activatorEvent

  if (typeof clientX !== "number" || typeof clientY !== "number") {
    return transform
  }

  return {
    ...transform,
    x:
      transform.x +
      clientX -
      activeNodeRect.left -
      overlayNodeRect.width / 2,
    y:
      transform.y +
      clientY -
      activeNodeRect.top -
      overlayNodeRect.height / 2,
  }
}

const canvasAwareCollisionDetection: CollisionDetection = (args) => {
  const pointerCollisions = pointerWithin(args)
  if (pointerCollisions.length > 0) return pointerCollisions

  return rectIntersection(args)
}

function PaletteField({
  type,
  label,
  icon: Icon,
}: {
  type: FormFieldType
  label: string
  icon: React.ComponentType<{ className?: string }>
}) {
  const { attributes, listeners, setActivatorNodeRef, setNodeRef, isDragging } =
    useDraggable({ id: `palette:${type}` })

  return (
    <button
      ref={setNodeRef}
      type="button"
      className="flex w-full items-center gap-2 rounded-md border bg-card px-3 py-2 text-left text-sm transition-colors hover:bg-muted"
      style={{ opacity: isDragging ? 0.4 : 1 }}
    >
      <span
        ref={setActivatorNodeRef}
        {...attributes}
        {...listeners}
        className="inline-flex cursor-grab touch-none select-none items-center justify-center rounded-sm p-0.5 text-muted-foreground active:cursor-grabbing"
      >
        <GripVerticalIcon className="size-4" />
      </span>
      <Icon className="size-4 text-muted-foreground" />
      <span>{label}</span>
    </button>
  )
}

function PaletteFieldPreview({
  label,
  icon: Icon,
}: {
  type: FormFieldType
  label: string
  icon: React.ComponentType<{ className?: string }>
}) {
  return (
    <div className="flex w-72 items-center gap-2 rounded-md border bg-card px-3 py-2 text-left text-sm shadow-lg">
      <span className="inline-flex touch-none select-none items-center justify-center rounded-sm p-0.5 text-muted-foreground">
        <GripVerticalIcon className="size-4" />
      </span>
      <Icon className="size-4 text-muted-foreground" />
      <span>{label}</span>
    </div>
  )
}

function CanvasField({
  field,
  rowSize,
  selected,
  onSelect,
  onDelete,
}: {
  field: FormField
  rowSize: number
  selected: boolean
  onSelect: () => void
  onDelete: () => void
}) {
  const { attributes, listeners, setActivatorNodeRef, setNodeRef, isDragging } =
    useDraggable({ id: `field:${field.id}` })
  const { setNodeRef: setBeforeDropRef, isOver: isOverBefore } = useDroppable({
    id: `field:${field.id}:before`,
  })
  const { setNodeRef: setAfterDropRef, isOver: isOverAfter } = useDroppable({
    id: `field:${field.id}:after`,
  })
  const isPageBreak = field.type === "page-break"

  return (
    <div className="relative">
      <div
        ref={setBeforeDropRef}
        className={cn(
          "pointer-events-none absolute z-10",
          isPageBreak ? "inset-x-1 top-0 h-1/2" : "inset-y-1 left-0 w-1/2"
        )}
        aria-hidden="true"
      />
      <div
        ref={setAfterDropRef}
        className={cn(
          "pointer-events-none absolute z-10",
          isPageBreak ? "inset-x-1 bottom-0 h-1/2" : "inset-y-1 right-0 w-1/2"
        )}
        aria-hidden="true"
      />
      {isOverBefore ? (
        <motion.div
          layoutId="form-builder-drop-line"
          className={cn(
            "absolute z-20 rounded-full bg-primary",
            isPageBreak ? "inset-x-2 -top-1 h-0.5" : "inset-y-2 left-0 w-0.5"
          )}
        />
      ) : null}
      {isOverAfter ? (
        <motion.div
          layoutId="form-builder-drop-line"
          className={cn(
            "absolute z-20 rounded-full bg-primary",
            isPageBreak ? "inset-x-2 -bottom-1 h-0.5" : "inset-y-2 right-0 w-0.5"
          )}
        />
      ) : null}
      <div
        ref={setNodeRef}
        data-form-builder-node
        data-row-size={rowSize}
        role="button"
        tabIndex={0}
        className={cn(
          "relative z-0 flex min-h-16 w-full items-center gap-2 rounded-md border p-3 text-left outline-none transition-colors",
          selected
            ? "border-primary bg-muted/40"
            : isPageBreak
              ? "border-dashed bg-muted/20"
              : "bg-card hover:bg-muted/30"
        )}
        style={{ opacity: isDragging ? 0.45 : 1 }}
        onClick={onSelect}
        onKeyDown={(event) => {
          if (event.key === "Enter" || event.key === " ") {
            event.preventDefault()
            onSelect()
          }
        }}
      >
        <span
          ref={setActivatorNodeRef}
          {...attributes}
          {...listeners}
          className="inline-flex size-7 shrink-0 cursor-grab touch-none select-none items-center justify-center rounded-md text-muted-foreground hover:bg-muted active:cursor-grabbing"
        >
          <GripVerticalIcon className="size-4" />
        </span>
        <div className="min-w-0 flex-1">
          <div className="truncate text-sm font-medium">
            {isPageBreak ? field.stepLabel || "Page break" : field.label}
          </div>
          <div className="truncate text-xs text-muted-foreground">
            {isPageBreak ? "Starts the next step" : field.key}
          </div>
        </div>
        {rowSize < 3 ? (
          <Badge variant="outline">{isPageBreak ? "step" : field.type}</Badge>
        ) : null}
        <Button
          type="button"
          size="icon-sm"
          variant="ghost"
          className="text-muted-foreground hover:text-destructive"
          onClick={(event) => {
            event.stopPropagation()
            onDelete()
          }}
        >
          <Trash2Icon className="size-4" />
          <span className="sr-only">Delete field</span>
        </Button>
      </div>
    </div>
  )
}

function CanvasFieldPreview({ field }: { field: FormField }) {
  const isPageBreak = field.type === "page-break"

  return (
    <div
      className={cn(
        "flex min-h-16 w-80 items-center gap-2 rounded-md border bg-card p-3 text-left shadow-lg",
        isPageBreak && "border-dashed bg-muted/20"
      )}
    >
      <span className="inline-flex size-7 shrink-0 touch-none select-none items-center justify-center rounded-md text-muted-foreground">
        <GripVerticalIcon className="size-4" />
      </span>
      <div className="min-w-0 flex-1">
        <div className="truncate text-sm font-medium">
          {isPageBreak ? field.stepLabel || "Page break" : field.label}
        </div>
        <div className="truncate text-xs text-muted-foreground">
          {isPageBreak ? "Starts the next step" : field.key}
        </div>
      </div>
      <Badge variant="outline">{isPageBreak ? "step" : field.type}</Badge>
      <span className="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground">
        <Trash2Icon className="size-4" />
      </span>
    </div>
  )
}

function FieldPalette() {
  return (
    <Card>
      <CardHeader className="pb-3">
        <CardTitle className="text-sm">Field types</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-2">
        {fieldTypes.map((item) => (
          <PaletteField key={item.type} {...item} />
        ))}
      </CardContent>
    </Card>
  )
}

function CanvasEndDropZone() {
  const { setNodeRef, isOver } = useDroppable({ id: "form-canvas-end" })

  return (
    <div ref={setNodeRef} className="relative min-h-14 md:col-span-6">
      {isOver ? (
        <motion.div
          layoutId="form-builder-drop-line"
          className="absolute inset-x-2 top-1 z-10 h-0.5 rounded-full bg-primary"
        />
      ) : null}
      <div
        className={cn(
          "flex h-full min-h-14 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground transition-colors",
          isOver ? "border-primary bg-muted/40" : "border-transparent"
        )}
      >
        {isOver ? "Drop to add a new row" : null}
      </div>
    </div>
  )
}

function FormCanvas({
  definition,
  selectedFieldId,
  onSelectField,
  onRemoveField,
}: {
  definition: FormDefinition
  selectedFieldId: string | null
  onSelectField: (id: string) => void
  onRemoveField: (id: string) => void
}) {
  const { setNodeRef, isOver } = useDroppable({ id: "form-canvas" })
  const fields = orderedFields(definition.fields)
  const rows = fieldRows(fields)

  return (
    <Card>
      <CardHeader className="pb-3">
        <div className="flex items-start justify-between gap-3">
          <div>
            <CardTitle className="text-sm">Canvas</CardTitle>
            <p className="mt-1 text-xs text-muted-foreground">
              Drag fields here, reorder them, then configure each field.
            </p>
          </div>
          <Badge variant="secondary">{fields.length} fields</Badge>
        </div>
      </CardHeader>
      <CardContent>
        <div
          ref={setNodeRef}
          className={cn(
            "min-h-96 rounded-md border border-dashed p-3 transition-colors",
            isOver ? "border-primary bg-muted/40" : "bg-background"
          )}
        >
          {fields.length === 0 ? (
            <div className="flex min-h-80 items-center justify-center rounded-md">
              <div className="text-center">
                <LayoutPanelTopIcon className="mx-auto size-8 text-muted-foreground" />
                <p className="mt-3 text-sm font-medium">Start with a field</p>
                <p className="mt-1 text-xs text-muted-foreground">
                  Drag an input, text block, or page break from the palette.
                </p>
              </div>
            </div>
          ) : (
            <div className="grid gap-2 md:grid-cols-6">
              {rows.map((row) =>
                row.map((field) => {
                  const isPageBreak = field.type === "page-break"
                  const span =
                    isPageBreak || row.length === 1
                      ? "md:col-span-6"
                      : row.length === 2
                        ? "md:col-span-3"
                        : "md:col-span-2"

                  return (
                    <div key={field.id} className={span}>
                      <CanvasField
                        field={field}
                        rowSize={row.length}
                        selected={field.id === selectedFieldId}
                        onSelect={() => onSelectField(field.id)}
                        onDelete={() => onRemoveField(field.id)}
                      />
                    </div>
                  )
                })
              )}
              <CanvasEndDropZone />
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  )
}

function FieldInspector({
  definition,
  selectedField,
  onUpdateField,
  onRemoveField,
}: {
  definition: FormDefinition
  selectedField: FormField | null
  onUpdateField: (id: string, next: Partial<FormField>) => void
  onRemoveField: (id: string) => void
}) {
  const previousFields = React.useMemo(() => {
    if (!selectedField) return []
    const fields = orderedFields(definition.fields)
    const index = fields.findIndex((field) => field.id === selectedField.id)
    return fields
      .slice(0, Math.max(0, index))
      .filter((field) => field.type !== "page-break" && field.type !== "static-text")
  }, [definition.fields, selectedField])

  const selectedRule =
    selectedField && selectedField.type !== "page-break"
      ? definition.visibilityRules?.find(
          (rule) => rule.targetField === selectedField.key
        )
      : undefined

  function updateVisibilityRule(next: FormVisibilityRule | null) {
    if (!selectedField) return
    const withoutCurrent = (definition.visibilityRules ?? []).filter(
      (rule) => rule.targetField !== selectedField.key
    )
    const visibilityRules = next ? [...withoutCurrent, next] : withoutCurrent
    onUpdateField(selectedField.id, { visibilityRules } as Partial<FormField>)
  }

  if (!selectedField) {
    return (
      <Card>
        <CardContent className="flex min-h-80 items-center justify-center p-6 text-center">
          <div>
            <PanelRightIcon className="mx-auto size-8 text-muted-foreground" />
            <p className="mt-3 text-sm font-medium">Select a field</p>
            <p className="mt-1 text-xs text-muted-foreground">
              Field configuration appears here.
            </p>
          </div>
        </CardContent>
      </Card>
    )
  }

  const usesOptions =
    selectedField.type === "select" ||
    selectedField.type === "multi-select" ||
    selectedField.type === "checkbox-group"

  return (
    <Card>
      <CardHeader className="pb-3">
        <CardTitle className="text-sm">Field settings</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-4">
        {selectedField.type === "page-break" ? (
          <div className="grid gap-2">
            <Label>Step label</Label>
            <Input
              value={selectedField.stepLabel ?? ""}
              placeholder="Review"
              onChange={(event) =>
                onUpdateField(selectedField.id, {
                  stepLabel: event.target.value,
                })
              }
            />
          </div>
        ) : (
          <>
            <div className="grid gap-2">
              <Label>Label</Label>
              <Input
                value={selectedField.label}
                onChange={(event) =>
                  onUpdateField(selectedField.id, {
                    label: event.target.value,
                    key: toFieldKey(event.target.value) || selectedField.key,
                  })
                }
              />
            </div>
            <div className="grid gap-2">
              <Label>Key</Label>
              <Input
                value={selectedField.key}
                onChange={(event) =>
                  onUpdateField(selectedField.id, {
                    key: toFieldKey(event.target.value),
                  })
                }
              />
            </div>
            <div className="grid gap-2">
              <Label>Type</Label>
              <Select
                value={selectedField.type}
                onValueChange={(value: FormFieldType) =>
                  onUpdateField(selectedField.id, { type: value })
                }
              >
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {fieldTypes.map((item) => (
                    <SelectItem key={item.type} value={item.type}>
                      {item.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            <div className="flex items-center gap-2">
              <Checkbox
                checked={Boolean(selectedField.required)}
                disabled={selectedField.type === "static-text"}
                onCheckedChange={(checked) =>
                  onUpdateField(selectedField.id, {
                    required: Boolean(checked),
                  })
                }
              />
              <Label>Required</Label>
            </div>
            {selectedField.type !== "static-text" ? (
              <div className="grid gap-2">
                <Label>Placeholder</Label>
                <Input
                  value={selectedField.placeholder ?? ""}
                  onChange={(event) =>
                    onUpdateField(selectedField.id, {
                      placeholder: event.target.value,
                    })
                  }
                />
              </div>
            ) : null}
            <div className="grid gap-2">
              <Label>
                {selectedField.type === "static-text"
                  ? "Text content"
                  : "Help text"}
              </Label>
              {selectedField.type === "static-text" ? (
                <Textarea
                  rows={4}
                  value={String(selectedField.defaultValue ?? "")}
                  onChange={(event) =>
                    onUpdateField(selectedField.id, {
                      defaultValue: event.target.value,
                      required: false,
                    })
                  }
                />
              ) : (
                <Input
                  value={selectedField.helpText ?? ""}
                  onChange={(event) =>
                    onUpdateField(selectedField.id, {
                      helpText: event.target.value,
                    })
                  }
                />
              )}
            </div>
          </>
        )}

        {usesOptions ? (
          <div className="grid gap-2">
            <Label>Options</Label>
            <Textarea
              rows={4}
              value={(selectedField.options ?? []).join("\n")}
              onChange={(event) =>
                onUpdateField(selectedField.id, {
                  options: event.target.value
                    .split("\n")
                    .map((item) => item.trim())
                    .filter(Boolean),
                })
              }
            />
          </div>
        ) : null}

        {selectedField.type !== "page-break" ? (
          <div className="grid gap-3 rounded-md border p-3">
            <div className="flex items-center justify-between gap-3">
              <Label className="text-sm">Visible if</Label>
              <Button
                type="button"
                size="sm"
                variant="ghost"
                disabled={!selectedRule}
                onClick={() => updateVisibilityRule(null)}
              >
                Clear
              </Button>
            </div>
            {previousFields.length === 0 ? (
              <p className="text-xs text-muted-foreground">
                Visibility rules can reference previous fields.
              </p>
            ) : (
              <>
                <Select
                  value={selectedRule?.dependsOnField ?? ""}
                  onValueChange={(dependsOnField) => {
                    updateVisibilityRule({
                      targetField: selectedField.key,
                      dependsOnField,
                      operator: selectedRule?.operator ?? "equals",
                      value: selectedRule?.value ?? "",
                    })
                  }}
                >
                  <SelectTrigger>
                    <SelectValue placeholder="Previous field" />
                  </SelectTrigger>
                  <SelectContent>
                    {previousFields.map((field) => (
                      <SelectItem key={field.key} value={field.key}>
                        {field.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <Select
                  value={selectedRule?.operator ?? "equals"}
                  disabled={!selectedRule}
                  onValueChange={(operator: FormVisibilityOperator) => {
                    if (!selectedRule) return
                    updateVisibilityRule({ ...selectedRule, operator })
                  }}
                >
                  <SelectTrigger>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {visibilityOperators.map((operator) => (
                      <SelectItem key={operator.value} value={operator.value}>
                        {operator.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                {selectedRule?.operator !== "is-empty" &&
                selectedRule?.operator !== "is-not-empty" ? (
                  <Input
                    value={String(selectedRule?.value ?? "")}
                    placeholder="Comparison value"
                    disabled={!selectedRule}
                    onChange={(event) => {
                      if (!selectedRule) return
                      updateVisibilityRule({
                        ...selectedRule,
                        value: event.target.value,
                      })
                    }}
                  />
                ) : null}
              </>
            )}
          </div>
        ) : null}

        <Button
          type="button"
          variant="ghost"
          className="w-fit px-2 text-muted-foreground hover:text-destructive"
          onClick={() => onRemoveField(selectedField.id)}
        >
          <Trash2Icon className="size-4" />
          Remove field
        </Button>
      </CardContent>
    </Card>
  )
}

function FormBuilderPropertiesSheet({
  open,
  sheetRef,
  definition,
  selectedField,
  onUpdateField,
  onRemoveField,
  onClose,
}: {
  open: boolean
  sheetRef: React.RefObject<HTMLDivElement | null>
  definition: FormDefinition
  selectedField: FormField | null
  onUpdateField: (id: string, next: Partial<FormField>) => void
  onRemoveField: (id: string) => void
  onClose: () => void
}) {
  const reduceMotion = useReducedMotion()

  return (
    <AnimatePresence>
      {open ? (
        <motion.aside
          ref={sheetRef}
          className="absolute inset-y-0 right-0 z-30 flex w-[min(24rem,calc(100%-1.5rem))] flex-col border-l bg-popover text-popover-foreground shadow-xl"
          initial={{ opacity: 0, x: reduceMotion ? 0 : 24 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: reduceMotion ? 0 : 24 }}
          transition={{ duration: reduceMotion ? 0 : 0.18, ease: "easeOut" }}
        >
          <div className="flex flex-col gap-1.5 border-b p-6 pr-14">
            <h2 className="font-heading text-sm font-medium text-foreground">
              Field properties
            </h2>
            <p className="text-xs/relaxed text-muted-foreground">
              Configure the selected field and its visibility rules.
            </p>
            <Button
              type="button"
              size="icon-sm"
              variant="ghost"
              className="absolute right-4 top-4"
              onClick={onClose}
            >
              <PanelRightIcon className="size-4" />
              <span className="sr-only">Close properties</span>
            </Button>
          </div>
          <ScrollArea className="min-h-0 flex-1">
            <div className="p-4">
              <FieldInspector
                definition={definition}
                selectedField={selectedField}
                onUpdateField={onUpdateField}
                onRemoveField={onRemoveField}
              />
            </div>
          </ScrollArea>
        </motion.aside>
      ) : null}
    </AnimatePresence>
  )
}

export function FormBuilder({
  definition,
  onDefinitionChange,
  className,
}: {
  definition: FormDefinition
  onDefinitionChange: (definition: FormDefinition) => void
  className?: string
}) {
  const reduceMotion = useReducedMotion()
  const isDesktopLayout = useMediaQuery("(min-width: 1024px)")
  const [selectedFieldId, setSelectedFieldId] = React.useState<string | null>(null)
  const [activeDragId, setActiveDragId] = React.useState<string | null>(null)
  const [mobileTab, setMobileTab] = React.useState("canvas")
  const sheetRef = React.useRef<HTMLDivElement | null>(null)
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
  )

  const selectedField =
    definition.fields.find((field) => field.id === selectedFieldId) ?? null

  React.useEffect(() => {
    if (!selectedFieldId) return

    function handlePointerDown(event: PointerEvent) {
      const target = event.target
      if (!(target instanceof Node)) return
      if (sheetRef.current?.contains(target)) return
      if (
        target instanceof Element &&
        target.closest("[data-form-builder-node]")
      ) {
        return
      }

      setSelectedFieldId(null)
    }

    document.addEventListener("pointerdown", handlePointerDown, true)

    return () => {
      document.removeEventListener("pointerdown", handlePointerDown, true)
    }
  }, [selectedFieldId])

  function setDefinition(next: FormDefinition) {
    onDefinitionChange(normalizeFormDefinition(next))
  }

  function updateField(id: string, next: Partial<FormField>) {
    if ("visibilityRules" in next) {
      onDefinitionChange({
        ...definition,
        visibilityRules: next.visibilityRules as FormVisibilityRule[],
      })
      return
    }

    setDefinition({
      ...definition,
      fields: definition.fields.map((field) =>
        field.id === id ? { ...field, ...next } : field
      ),
    })
  }

  function removeField(id: string) {
    const removed = definition.fields.find((field) => field.id === id)
    setDefinition({
      ...definition,
      fields: definition.fields.filter((field) => field.id !== id),
      visibilityRules: (definition.visibilityRules ?? []).filter(
        (rule) =>
          rule.targetField !== removed?.key && rule.dependsOnField !== removed?.key
      ),
    })
    setSelectedFieldId((current) => (current === id ? null : current))
  }

  function handleDragStart(event: DragStartEvent) {
    setActiveDragId(String(event.active.id))
  }

  function handleDragEnd(event: DragEndEvent) {
    const activeId = String(event.active.id)
    const overId = String(event.over?.id ?? "")
    setActiveDragId(null)
    if (!overId) return

    const fields = orderedFields(definition.fields)
    let nextField: FormField | null = null
    let nextFields = fields

    if (activeId.startsWith("palette:")) {
      const type = activeId.replace("palette:", "") as FormFieldType
      nextField = createFormField(type, definition)
      setSelectedFieldId(nextField.id)
    } else if (activeId.startsWith("field:")) {
      const id = activeId.replace("field:", "")
      nextField = fields.find((field) => field.id === id) ?? null
      nextFields = fields.filter((field) => field.id !== id)
    }

    if (!nextField) return

    let insertIndex = nextFields.length
    if (overId.startsWith("field:")) {
      const [, overFieldId, position = "before"] = overId.split(":")
      const foundIndex = nextFields.findIndex((field) => field.id === overFieldId)
      if (foundIndex >= 0) insertIndex = position === "after" ? foundIndex + 1 : foundIndex
    } else if (overId === "form-canvas" || overId === "form-canvas-end") {
      insertIndex = nextFields.length
    }

    nextFields.splice(insertIndex, 0, nextField)
    setDefinition({ ...definition, fields: applyFieldLayout(nextFields) })
  }

  const activePaletteType = activeDragId?.startsWith("palette:")
    ? (activeDragId.replace("palette:", "") as FormFieldType)
    : null
  const activePaletteField = activePaletteType
    ? fieldTypes.find((field) => field.type === activePaletteType)
    : null
  const activeCanvasField = activeDragId?.startsWith("field:")
    ? definition.fields.find(
        (field) => field.id === activeDragId.replace("field:", "")
      )
    : null

  const builderGrid = (
    <DndContext
      sensors={sensors}
      collisionDetection={canvasAwareCollisionDetection}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
    >
      {isDesktopLayout ? (
        <div className="grid gap-4 lg:grid-cols-[15rem_minmax(0,1fr)]">
          <FieldPalette />
          <FormCanvas
            definition={definition}
            selectedFieldId={selectedFieldId}
            onSelectField={setSelectedFieldId}
            onRemoveField={removeField}
          />
        </div>
      ) : (
        <Tabs
          value={mobileTab}
          onValueChange={setMobileTab}
        >
          <TabsList className="grid w-full grid-cols-2">
            <TabsTrigger value="palette">Palette</TabsTrigger>
            <TabsTrigger value="canvas">Canvas</TabsTrigger>
          </TabsList>
          <AnimatePresence mode="wait" initial={false}>
            <motion.div
              key={mobileTab}
              initial={{ opacity: 0, x: reduceMotion ? 0 : 12 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: reduceMotion ? 0 : -12 }}
              transition={{ duration: reduceMotion ? 0 : 0.18 }}
              className="mt-3"
            >
              {mobileTab === "palette" ? (
                <TabsContent value="palette" forceMount>
                  <FieldPalette />
                </TabsContent>
              ) : (
                <TabsContent value="canvas" forceMount>
                  <FormCanvas
                    definition={definition}
                    selectedFieldId={selectedFieldId}
                    onSelectField={(id) => {
                      setSelectedFieldId(id)
                    }}
                    onRemoveField={removeField}
                  />
                </TabsContent>
              )}
            </motion.div>
          </AnimatePresence>
        </Tabs>
      )}

      {typeof document !== "undefined"
        ? createPortal(
            <DragOverlay modifiers={[snapOverlayToCursor]}>
              {activePaletteField ? (
                <PaletteFieldPreview {...activePaletteField} />
              ) : activeCanvasField ? (
                <CanvasFieldPreview field={activeCanvasField} />
              ) : null}
            </DragOverlay>,
            document.body
          )
        : null}
    </DndContext>
  )

  return (
    <div className={cn("relative overflow-hidden rounded-md border bg-card p-3", className)}>
      <div className="mb-3 flex flex-wrap items-start justify-between gap-3">
        <div>
          <h2 className="text-lg font-semibold">
            {definition.title || "Form builder"}
          </h2>
          {definition.description ? (
            <p className="mt-1 text-sm text-muted-foreground">
              {definition.description}
            </p>
          ) : null}
        </div>
        <div className="flex items-center gap-2">
          <Badge variant="outline">{buildFormPages(definition).length} pages</Badge>
          <FormPreviewDialog definition={definition} />
        </div>
      </div>
      {builderGrid}
      <FormBuilderPropertiesSheet
        open={Boolean(selectedField)}
        sheetRef={sheetRef}
        definition={definition}
        selectedField={selectedField}
        onUpdateField={updateField}
        onRemoveField={removeField}
        onClose={() => setSelectedFieldId(null)}
      />
    </div>
  )
}

function FieldError({ error }: { error?: string }) {
  if (!error) return null

  return <p className="text-xs text-destructive">{error}</p>
}

function FieldHelp({ children }: { children?: React.ReactNode }) {
  if (!children) return null

  return <p className="text-xs text-muted-foreground">{children}</p>
}

function parseDate(value: unknown) {
  if (typeof value !== "string" || !value) return undefined
  const [year, month, day] = value.split("-").map(Number)
  if (!year || !month || !day) return undefined
  return new Date(year, month - 1, day, 12)
}

function MultiSelectCombobox({
  field,
  value,
  onChange,
}: {
  field: FormField
  value: unknown
  onChange: (value: string[]) => void
}) {
  const anchor = useComboboxAnchor()
  const selected = Array.isArray(value) ? value.map(String) : []
  const options = field.options ?? []

  return (
    <Combobox
      multiple
      autoHighlight
      items={options}
      value={selected}
      onValueChange={(nextValue) => {
        onChange(Array.isArray(nextValue) ? nextValue.map(String) : [])
      }}
    >
      <ComboboxChips ref={anchor} className="w-full">
        <ComboboxValue>
          {(values) => (
            <React.Fragment>
              {(values as string[]).map((item) => (
                <ComboboxChip key={item}>{item}</ComboboxChip>
              ))}
              <ComboboxChipsInput placeholder={field.placeholder ?? "Select options"} />
            </React.Fragment>
          )}
        </ComboboxValue>
      </ComboboxChips>
      <ComboboxContent anchor={anchor}>
        <ComboboxEmpty>No options found.</ComboboxEmpty>
        <ComboboxList>
          {(item) => (
            <ComboboxItem key={String(item)} value={String(item)}>
              {String(item)}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}

function renderFormField({
  field,
  value,
  error,
  onChange,
}: {
  field: FormField
  value: unknown
  error?: string
  onChange: (value: unknown) => void
}) {
  const fieldId = `form-field-${field.id}`
  const label = (
    <Label htmlFor={fieldId}>
      {field.label}
      {field.required ? <span className="ml-1 text-destructive">*</span> : null}
    </Label>
  )

  if (field.type === "static-text") {
    return (
      <div className="rounded-md border bg-muted/20 p-3">
        <div className="font-medium">{field.label}</div>
        <p className="mt-1 whitespace-pre-wrap text-sm text-muted-foreground">
          {String(field.defaultValue ?? field.helpText ?? "")}
        </p>
      </div>
    )
  }

  if (field.type === "textarea") {
    return (
      <div className="grid gap-2">
        {label}
        <Textarea
          id={fieldId}
          placeholder={field.placeholder}
          value={typeof value === "string" ? value : ""}
          onChange={(event) => onChange(event.target.value)}
        />
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "date") {
    return (
      <div className="grid gap-2">
        {label}
        <DatePickerField
          buttonId={fieldId}
          className="w-full justify-between text-left font-normal data-[empty=true]:text-muted-foreground"
          date={parseDate(value)}
          placeholder={field.placeholder ?? "Pick a date"}
          onSelect={() => undefined}
          onSelectDateString={(dateString: string | undefined) =>
            onChange(dateString ?? "")
          }
          closeOnSelect
        />
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "datetime") {
    return (
      <div className="grid gap-2">
        {label}
        <Input
          id={fieldId}
          type="datetime-local"
          value={typeof value === "string" ? value : ""}
          onChange={(event) => onChange(event.target.value)}
        />
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "select") {
    return (
      <div className="grid gap-2">
        {label}
        <Select
          value={typeof value === "string" ? value : ""}
          onValueChange={onChange}
        >
          <SelectTrigger id={fieldId}>
            <SelectValue placeholder={field.placeholder ?? "Select option"} />
          </SelectTrigger>
          <SelectContent>
            {(field.options ?? []).map((option) => (
              <SelectItem key={option} value={option}>
                {option}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "multi-select") {
    return (
      <div className="grid gap-2">
        {label}
        <MultiSelectCombobox
          field={field}
          value={value}
          onChange={onChange}
        />
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "checkbox-group") {
    const selected = Array.isArray(value) ? value.map(String) : []
    return (
      <div className="grid gap-2">
        {label}
        <div className="grid gap-2 rounded-md border p-3">
          {(field.options ?? []).map((option) => (
            <label key={option} className="flex items-center gap-2 text-sm">
              <Checkbox
                checked={selected.includes(option)}
                onCheckedChange={(checked) => {
                  const set = new Set(selected)
                  if (checked) set.add(option)
                  else set.delete(option)
                  onChange(Array.from(set))
                }}
              />
              {option}
            </label>
          ))}
        </div>
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "checkbox") {
    return (
      <div className="grid gap-2">
        <div className="flex items-center gap-2">
          <Checkbox
            id={fieldId}
            checked={Boolean(value)}
            onCheckedChange={(checked) => onChange(Boolean(checked))}
          />
          {label}
        </div>
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  if (field.type === "number") {
    return (
      <div className="grid gap-2">
        {label}
        <Input
          id={fieldId}
          type="number"
          placeholder={field.placeholder}
          value={typeof value === "number" || typeof value === "string" ? value : ""}
          onChange={(event) => onChange(event.target.value)}
        />
        <FieldHelp>{field.helpText}</FieldHelp>
        <FieldError error={error} />
      </div>
    )
  }

  return (
    <div className="grid gap-2">
      {label}
      <Input
        id={fieldId}
        type={field.type === "email" ? "email" : "text"}
        placeholder={field.placeholder}
        value={typeof value === "string" ? value : ""}
        onChange={(event) => onChange(event.target.value)}
      />
      <FieldHelp>{field.helpText}</FieldHelp>
      <FieldError error={error} />
    </div>
  )
}

export function FormRenderer({
  definition,
  response,
  renderDefinition,
  renderMode = "submitted-version",
  actor,
  mode = "create",
  value,
  onValueChange,
  onSubmit,
  submitLabel = "Submit",
  footerContainer,
  footerClassName,
  formId,
  className,
}: {
  definition: FormDefinition
  response?: FormResponseRecord
  renderDefinition?: FormDefinition
  renderMode?: FormResponseRenderMode
  actor?: FormActor
  mode?: "create" | "edit"
  value?: FormResponses
  onValueChange?: (responses: FormResponses) => void
  onSubmit?: (response: FormResponseRecord) => void
  submitLabel?: string
  footerContainer?: HTMLElement | null
  footerClassName?: string
  formId?: string
  className?: string
}) {
  const reduceMotion = useReducedMotion()
  const resolvedDefinition = React.useMemo(
    () =>
      response
        ? resolveFormDefinitionForResponse(response, renderDefinition ?? definition, renderMode)
        : renderDefinition ?? definition,
    [definition, renderDefinition, renderMode, response]
  )
  const normalizedDefinition = React.useMemo(
    () => normalizeFormDefinition(resolvedDefinition),
    [resolvedDefinition]
  )
  const pages = React.useMemo(
    () => buildFormPages(normalizedDefinition),
    [normalizedDefinition]
  )
  const initialValue = React.useMemo(
    () => value ?? (response ? getFormResponseValues(response) : initialResponses(normalizedDefinition)),
    [normalizedDefinition, response, value]
  )
  const [responses, setResponses] = React.useState<FormResponses>(() =>
    initialValue
  )
  const [errors, setErrors] = React.useState<Record<string, string>>({})
  const [pageIndex, setPageIndex] = React.useState(0)
  const shouldNotifyValueChange = React.useRef(false)
  const currentPage = pages[Math.min(pageIndex, Math.max(0, pages.length - 1))]
  const isFinalPage = pageIndex >= pages.length - 1
  const visibility = getFieldVisibility(normalizedDefinition, responses)
  const isRenderingLatest =
    Boolean(response && renderMode === "latest-version" && renderDefinition) &&
    formVersionFor(renderDefinition ?? definition) !== response?.formVersion

  React.useEffect(() => {
    setResponses(initialValue)
    shouldNotifyValueChange.current = false
  }, [initialValue])

  React.useEffect(() => {
    if (!shouldNotifyValueChange.current) return
    shouldNotifyValueChange.current = false
    onValueChange?.(responses)
  }, [onValueChange, responses])

  function updateResponse(key: string, nextValue: unknown) {
    shouldNotifyValueChange.current = true
    setResponses((current) => {
      const next = { ...current, [key]: nextValue }
      return next
    })
  }

  function validateCurrentPage() {
    const fieldKeys = currentPage?.fields.map((field) => field.key) ?? []
    const nextErrors = validateFormResponses(
      normalizedDefinition,
      responses,
      fieldKeys
    )
    setErrors(nextErrors)
    return Object.keys(nextErrors).length === 0
  }

  function handleSubmit(event: React.FormEvent) {
    event.preventDefault()
    if (!currentPage) return

    if (!isFinalPage) {
      if (validateCurrentPage()) setPageIndex((current) => current + 1)
      return
    }

    const nextErrors = validateFormResponses(normalizedDefinition, responses)
    setErrors(nextErrors)
    if (Object.keys(nextErrors).length === 0) {
      onSubmit?.(
        mode === "edit" && response
          ? updateFormResponseRecord(response, responses, { actor })
          : createFormResponseRecord(normalizedDefinition, responses, { actor })
      )
    }
  }

  const footer = (
    <div
      className={cn(
        "flex justify-end gap-2 border-t p-4",
        footerContainer && "border-t-0 p-0",
        footerClassName
      )}
    >
      {pages.length > 1 ? (
        <Button
          type="button"
          variant="outline"
          disabled={pageIndex === 0}
          onClick={() => setPageIndex((current) => Math.max(0, current - 1))}
        >
          Back
        </Button>
      ) : null}
      <Button type="submit" form={formId}>
        {pages.length > 1 && !isFinalPage ? "Next" : submitLabel}
      </Button>
    </div>
  )

  return (
    <form
      id={formId}
      onSubmit={handleSubmit}
      className={cn("rounded-md border bg-card", className)}
    >
      <div className="border-b p-4">
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div>
            <h2 className="text-lg font-semibold">
              {normalizedDefinition.title ?? "Generated form"}
            </h2>
            {normalizedDefinition.description ? (
              <p className="mt-1 text-sm text-muted-foreground">
                {normalizedDefinition.description}
              </p>
            ) : null}
          </div>
          {pages.length > 1 ? (
            <Badge variant="secondary">
              Step {pageIndex + 1} of {pages.length}
            </Badge>
          ) : null}
        </div>
        {isRenderingLatest ? (
          <div className="mt-4 rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
            Rendering this saved response against the latest form version. Stored response data remains unchanged.
          </div>
        ) : null}
        {pages.length > 1 ? (
          <Stepper
            steps={pages.map((page) => ({
              id: page.id,
              title: page.label,
              description: `${page.fields.length} fields`,
            }))}
            currentStep={pageIndex}
            className="mt-4"
          />
        ) : null}
      </div>
      <div className="overflow-hidden p-4">
        <AnimatePresence mode="wait" initial={false}>
          <motion.div
            key={currentPage?.id ?? "empty"}
            initial={{ opacity: 0, x: reduceMotion ? 0 : 12 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: reduceMotion ? 0 : -12 }}
            transition={{ duration: reduceMotion ? 0 : 0.18 }}
            className="grid gap-4 md:grid-cols-2"
          >
            {(currentPage?.fields ?? [])
              .filter((field) => visibility[field.key])
              .map((field) => (
                <div
                  key={field.id}
                  className={cn(
                    (field.type === "textarea" ||
                      field.type === "static-text" ||
                      field.type === "checkbox-group" ||
                      field.type === "multi-select") &&
                      "md:col-span-2"
                  )}
                >
                  {renderFormField({
                    field,
                    value: responses[field.key],
                    error: errors[field.key],
                    onChange: (nextValue) => updateResponse(field.key, nextValue),
                  })}
                </div>
              ))}
          </motion.div>
        </AnimatePresence>
      </div>
      {footerContainer ? createPortal(footer, footerContainer) : footer}
    </form>
  )
}

export function FormPreviewDialog({
  definition,
}: {
  definition: FormDefinition
}) {
  const [open, setOpen] = React.useState(false)
  const isSmallScreen = useIsSmallScreen()
  const formId = React.useId()
  const [drawerFooterActions, setDrawerFooterActions] =
    React.useState<HTMLDivElement | null>(null)

  const trigger = (
    <Button type="button" variant="outline" size="sm">
      <EyeIcon className="size-4" />
      Preview
    </Button>
  )

  if (isSmallScreen) {
    return (
      <Drawer open={open} onOpenChange={setOpen}>
        <Button
          type="button"
          variant="outline"
          size="sm"
          onClick={() => setOpen(true)}
        >
          <EyeIcon className="size-4" />
          Preview
        </Button>
        <DrawerContent className="flex h-[92svh] max-h-[92svh] flex-col overflow-hidden">
          <DrawerHeader className="shrink-0 text-left">
            <DrawerTitle>Form preview</DrawerTitle>
            <DrawerDescription>
              Test the generated form before saving.
            </DrawerDescription>
          </DrawerHeader>
          <ScrollArea className="min-h-0 flex-1">
            <div className="px-4 pb-4">
              <FormRenderer
                definition={definition}
                formId={formId}
                footerContainer={drawerFooterActions}
                footerClassName="grid grid-cols-2 gap-2 [&>button:only-child]:col-span-2"
              />
            </div>
          </ScrollArea>
          <DrawerFooter className="shrink-0 border-t">
            <div ref={setDrawerFooterActions} />
            <Button type="button" variant="outline" onClick={() => setOpen(false)}>
              Close
            </Button>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    )
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>{trigger}</DialogTrigger>
      <DialogContent className="flex h-[min(90svh,46rem)] overflow-hidden sm:max-w-4xl sm:flex-col">
        <DialogHeader className="shrink-0 pr-8">
          <DialogTitle>Form preview</DialogTitle>
          <DialogDescription>
            Test the generated form before saving the JSON definition.
          </DialogDescription>
        </DialogHeader>
        <ScrollArea className="min-h-0 flex-1">
          <div className="pr-3 pb-1">
            <FormRenderer definition={definition} />
          </div>
        </ScrollArea>
        <DialogFooter className="shrink-0 border-t pt-3">
          <Button type="button" variant="outline" onClick={() => setOpen(false)}>
            Close
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

export const exampleFormDefinition: FormDefinition = normalizeFormDefinition({
  id: "project-intake",
  version: 1,
  title: "Project intake",
  description: "Capture enough context to triage a new request.",
  metadata: {
    author: {
      id: "user-liam",
      name: "Liam Corrigan",
      email: "liam@corr.sh",
    },
    createdAt: "2026-05-01T09:00:00.000Z",
    modifiedAt: "2026-05-10T14:30:00.000Z",
    modifiedBy: {
      id: "user-liam",
      name: "Liam Corrigan",
      email: "liam@corr.sh",
    },
    publishedAt: "2026-05-12T10:00:00.000Z",
    status: "published",
  },
  fields: [
    {
      id: "field-name",
      key: "contact_name",
      label: "Contact name",
      type: "text",
      required: true,
      placeholder: "Liam Corrigan",
    },
    {
      id: "field-email",
      key: "contact_email",
      label: "Email",
      type: "email",
      required: true,
      placeholder: "liam@example.com",
    },
    {
      id: "field-budget",
      key: "budget",
      label: "Budget range",
      type: "select",
      options: ["Under 5k", "5k-15k", "15k+"],
      placeholder: "Select budget",
    },
    {
      id: "field-type",
      key: "project_type",
      label: "Project type",
      type: "select",
      required: true,
      options: ["Component", "Block", "Integration", "Consultancy"],
      placeholder: "Select project type",
    },
    {
      id: "field-page-break",
      key: "page_break_1",
      label: "Page break",
      type: "page-break",
      stepLabel: "Scope",
    },
    {
      id: "field-summary",
      key: "summary",
      label: "Project summary",
      type: "textarea",
      required: true,
      placeholder: "What are you trying to build?",
      helpText: "Include goals, constraints, and any current tooling.",
    },
    {
      id: "field-features",
      key: "features",
      label: "Needed features",
      type: "checkbox-group",
      options: ["Design system", "Dashboard", "Forms", "Analytics"],
      helpText: "Pick every feature that applies.",
    },
  ],
  visibilityRules: [
    {
      targetField: "budget",
      dependsOnField: "project_type",
      operator: "is-not-empty",
    },
  ],
})

export function FormBuilderDemo() {
  const [definition, setDefinition] = React.useState<FormDefinition>(
    exampleFormDefinition
  )

  return <FormBuilder definition={definition} onDefinitionChange={setDefinition} />
}
```

### `registry/corr/components/date-picker-field.tsx`

```tsx
"use client"

import * as React from "react"
import { addDays, format } from "date-fns"
import { CalendarIcon, ChevronDownIcon } from "lucide-react"
import { type DateRange } from "react-day-picker"

import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

function toLocalDateString(value: Date) {
  const year = value.getFullYear()
  const month = String(value.getMonth() + 1).padStart(2, "0")
  const day = String(value.getDate()).padStart(2, "0")
  return `${year}-${month}-${day}`
}

function toLocalSafeDate(value: Date) {
  return new Date(
    value.getFullYear(),
    value.getMonth(),
    value.getDate(),
    12,
    0,
    0,
    0
  )
}

export function DatePickerField({
  date,
  onSelect,
  onSelectDateString,
  placeholder = "Pick a date",
  disabled,
  captionLayout,
  closeOnSelect = false,
  buttonId,
  displayValue,
  className,
}: {
  date?: Date
  onSelect: (date: Date | undefined) => void
  onSelectDateString?: (dateString: string | undefined) => void
  placeholder?: string
  disabled?: boolean
  captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
  closeOnSelect?: boolean
  buttonId?: string
  displayValue?: (date: Date) => string
  className?: string
}) {
  const [open, setOpen] = React.useState(false)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          id={buttonId}
          variant="outline"
          disabled={disabled}
          data-empty={!date}
          className={
            className ??
            "w-[212px] justify-between text-left font-normal data-[empty=true]:text-muted-foreground"
          }
        >
          {date ? (
            displayValue ? (
              displayValue(date)
            ) : (
              format(date, "PPP")
            )
          ) : (
            <span>{placeholder}</span>
          )}
          <ChevronDownIcon />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0" align="start">
        <Calendar
          mode="single"
          selected={date}
          captionLayout={captionLayout}
          onSelect={(selected) => {
            const normalized = selected ? toLocalSafeDate(selected) : undefined
            onSelect(normalized)
            onSelectDateString?.(
              normalized ? toLocalDateString(normalized) : undefined
            )
            if (closeOnSelect) setOpen(false)
          }}
          defaultMonth={date}
        />
      </PopoverContent>
    </Popover>
  )
}

export function DatePickerRangeField({
  date,
  onSelect,
  label = "Date Picker Range",
  buttonId = "date-picker-range",
}: {
  date?: DateRange
  onSelect: (date: DateRange | undefined) => void
  label?: string
  buttonId?: string
}) {
  return (
    <Field className="mx-auto w-60">
      <FieldLabel htmlFor={buttonId}>{label}</FieldLabel>
      <Popover>
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            id={buttonId}
            className="justify-start px-2.5 font-normal"
          >
            <CalendarIcon />
            {date?.from ? (
              date.to ? (
                <>
                  {format(date.from, "LLL dd, y")} -{" "}
                  {format(date.to, "LLL dd, y")}
                </>
              ) : (
                format(date.from, "LLL dd, y")
              )
            ) : (
              <span>Pick a date</span>
            )}
          </Button>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0" align="start">
          <Calendar
            mode="range"
            defaultMonth={date?.from}
            selected={date}
            onSelect={onSelect}
            numberOfMonths={2}
          />
        </PopoverContent>
      </Popover>
    </Field>
  )
}

export function DatePickerTimeField({
  date,
  onSelect,
  time,
  onTimeChange,
  defaultTime = "10:30:00",
}: {
  date?: Date
  onSelect: (date: Date | undefined) => void
  time?: string
  onTimeChange?: (time: string) => void
  defaultTime?: string
}) {
  const [open, setOpen] = React.useState(false)

  return (
    <FieldGroup className="mx-auto max-w-xs flex-row">
      <Field>
        <FieldLabel htmlFor="date-picker-optional">Date</FieldLabel>
        <Popover open={open} onOpenChange={setOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              id="date-picker-optional"
              className="w-32 justify-between font-normal"
            >
              {date ? format(date, "PPP") : "Select date"}
              <ChevronDownIcon />
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-auto overflow-hidden p-0" align="start">
            <Calendar
              mode="single"
              selected={date}
              captionLayout="dropdown"
              defaultMonth={date}
              onSelect={(selected) => {
                onSelect(selected)
                setOpen(false)
              }}
            />
          </PopoverContent>
        </Popover>
      </Field>
      <Field className="w-32">
        <FieldLabel htmlFor="time-picker-optional">Time</FieldLabel>
        <Input
          type="time"
          id="time-picker-optional"
          step="1"
          value={time}
          defaultValue={time === undefined ? defaultTime : undefined}
          onChange={(event) => onTimeChange?.(event.target.value)}
          className="appearance-none bg-background [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
        />
      </Field>
    </FieldGroup>
  )
}

export function DatePickerRangeDemo() {
  const [date, setDate] = React.useState<DateRange | undefined>({
    from: new Date(new Date().getFullYear(), 0, 20),
    to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
  })

  return <DatePickerRangeField date={date} onSelect={setDate} />
}
```
