# Site massing

> Compare building sizes, check site limits and save design options in plan or 3D.

- Live example: https://floorkit-examples.ploterr.com/floorkit/playbooks/site-massing
- Docs page: https://docs.ploterr.com/floorkit/playbooks/site-massing
- Category: Playbooks
- API: `ProjectManager`, `ManagedPlanEditor`, `massBuildingCommand`, `evaluateZoning`
- Tags: site, massing, feasibility, options, zoning
- Package: `@opengeometry/floorkit`
- Source: `examples/src/playbooks/site-massing.example.ts`

Each example is one module exporting `meta` and `setup(ctx)`. The examples shell renders it in 3D and as a 2D plan (or on a sheet surface for drawing examples) and builds its controls from the element schema.

## Source

### examples/src/playbooks/site-massing.example.ts

```ts
import type { ExampleMeta, ExampleResult, SurfaceContext } from '../app/lib/example';
import { mountStudyPage } from './site-massing-page';

export const meta: ExampleMeta = {
  title: 'Site massing',
  description: 'Compare building sizes, check site limits and save design options in plan or 3D.',
  tags: ['site', 'massing', 'feasibility', 'options', 'zoning'],
  api: ['ProjectManager', 'ManagedPlanEditor', 'massBuildingCommand', 'evaluateZoning'],
  surface: 'dom',
};

export async function setup({ container }: SurfaceContext): Promise<ExampleResult> {
  const page = mountStudyPage(container);
  try {
    const workspace = await page.ready;
    return {
      controls: false,
      expose: {
        workspace,
        get manager() {
          return workspace.manager;
        },
        get session() {
          return workspace.manager.session;
        },
        get editor() {
          return workspace.plan;
        },
      },
      dispose: page.dispose,
    };
  } catch (error) {
    page.dispose();
    throw error;
  }
}
```

### examples/src/app/lib/example.ts

```ts
import type * as THREE from 'three';
import type { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import type { createModelRenderer, EditorChange, OPControl, VectorDrawing, WallNetwork } from '@opengeometry/floorkit';

export type EditTarget = THREE.Object3D & { ogType: string };

export type PlanRenderer = Awaited<ReturnType<typeof createModelRenderer>>;

export interface ExampleMeta {
  title: string;
  description: string;
  tags?: readonly string[];
  api?: readonly string[];
  camera?: { position: [number, number, number]; target: [number, number, number] };
  view?: '3d' | '2d';
  surface?: 'dom';
  grid?: false | { size: number; y?: number };
  notes?: readonly string[];
}

export interface SurfaceContext {
  container: HTMLElement;
  refresh: () => void;
}

export interface ExampleContext extends SurfaceContext {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  controls: OrbitControls;
  renderer: THREE.WebGLRenderer;
  onFrame: (callback: () => void) => void;
}

export interface PlanSource {
  getDrawing: () => VectorDrawing;
  getElementById?: (ogid: string) => unknown;
  getChangedDrawing?: (ogids: readonly string[]) => VectorDrawing;
  subscribeInvalidation?: (callback: (ogids: readonly string[]) => void) => () => void;
}

export interface PlanElement {
  ogid: string;
}

export type InputControl =
  | { kind: 'text'; input: 'date' | 'password' }
  | { kind: 'file'; accept: string };

export interface ValueControl {
  key: string;
  label: string;
  group?: string;
  control: OPControl | InputControl;
  get: () => unknown;
  set?: (value: unknown) => void;
  commit?: (value: unknown) => void;
}

export type ActionIcon = 'undo' | 'redo';

export interface ActionControl {
  key: string;
  label: string;
  group?: string;
  icon?: ActionIcon;
  run: () => unknown;
}

export type ControlOverride = ValueControl | ActionControl;

export type Verdict = 'pass' | 'fail';

export interface ReportSection {
  title: string;
  verdict?: Verdict;
  columns?: readonly string[];
  rows?: ReadonlyArray<{ cells: ReadonlyArray<string | number>; verdict?: Verdict }>;
  note?: string;
}

export interface EditChoice {
  label: string;
  target: EditTarget;
}

export interface ExampleResult {
  edit?: EditTarget | (() => EditTarget | undefined);
  targets?: readonly EditChoice[] | (() => readonly EditChoice[]);
  pick?: boolean;
  plan?: WallNetwork | readonly PlanElement[] | PlanSource;
  controls?: false | readonly ControlOverride[] | ((target: EditTarget | undefined) => readonly ControlOverride[]);
  report?: () => readonly ReportSection[];
  onEdit?: (change: EditorChange) => void;
  onChange?: () => void;
  onPlan?: (renderer: PlanRenderer) => void;
  expose?: Record<string, unknown>;
  dispose?: () => void;
}

export interface ExampleModule {
  meta: ExampleMeta;
  setup: (context: ExampleContext) => Promise<ExampleResult> | ExampleResult;
}
```

### examples/src/playbooks/site-massing-page.tsx

```ts
import { useEffect, useReducer, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Button } from '../app/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '../app/components/ui/tabs';
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from '../app/components/ui/sheet';
import { useIsMobile } from '../app/hooks/use-mobile';
import { StudyControls, type StudyPanel } from './site-massing-controls';
import { StudySummary } from './site-massing-summary';
import { SiteMassingWorkspace } from './site-massing-workspace';

interface StudyPageProps {
  ready: (workspace: SiteMassingWorkspace) => void;
  failed: (error: unknown) => void;
}

function StudyPage({ ready, failed }: StudyPageProps) {
  const scene = useRef<HTMLDivElement>(null);
  const plan = useRef<HTMLDivElement>(null);
  const stage = useRef<HTMLElement>(null);
  const [workspace, setWorkspace] = useState<SiteMassingWorkspace>();
  const [error, setError] = useState<string>();
  const [controlsOpen, setControlsOpen] = useState(window.innerWidth >= 768);
  const [panel, setPanel] = useState<StudyPanel>('edit');
  const [optionName, setOptionName] = useState('Baseline');
  const [, refresh] = useReducer((count) => count + 1, 0);
  const mobile = useIsMobile();
  const [narrowStage, setNarrowStage] = useState(false);
  const compact = mobile || narrowStage;
  useEffect(() => {
    const host = stage.current;
    if (!host) return;
    const sizes = new ResizeObserver(() => setNarrowStage(host.clientWidth < 640));
    sizes.observe(host);
    return () => sizes.disconnect();
  }, []);
  useEffect(() => {
    if (compact) setControlsOpen(false);
  }, [compact]);
  useEffect(() => {
    const sceneHost = scene.current;
    const planHost = plan.current;
    if (!sceneHost || !planHost) return;
    let active = true;
    const study = new SiteMassingWorkspace({ scene: sceneHost, plan: planHost }, () => {
      if (active) refresh();
    });
    void study
      .mount()
      .then(() => {
        if (!active) return;
        setWorkspace(study);
        ready(study);
      })
      .catch((error) => {
        if (active) {
          setError(String(error));
          failed(error);
        }
      });
    return () => {
      active = false;
      study.dispose();
    };
  }, [ready, failed]);
  const run = (action: () => unknown) => {
    setError(undefined);
    try {
      Promise.resolve(action())
        .then(() => refresh())
        .catch((caught: unknown) => {
          setError(caught instanceof Error ? caught.message : String(caught));
          refresh();
        });
      refresh();
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
    }
  };
  const controls = workspace ? (
    <StudyControls
      workspace={workspace}
      run={run}
      panel={panel}
      onPanelChange={setPanel}
      name={optionName}
      onNameChange={setOptionName}
      onDraw={() => {
        if (compact) setControlsOpen(false);
        run(() => workspace.draw());
      }}
    />
  ) : null;
  return (
    <main ref={stage} className="flex h-full min-h-0 flex-col" data-site-study>
      {workspace && <StudySummary workspace={workspace} />}
      <div className="flex shrink-0 flex-wrap items-center gap-2 border-b px-4 py-2">
        <Tabs
          value={workspace?.view ?? '3d'}
          onValueChange={(view) => {
            if (view === 'plan' || view === '3d') run(() => workspace?.setView(view));
          }}
        >
          <TabsList aria-label="Site study view">
            <TabsTrigger value="3d">3D</TabsTrigger>
            <TabsTrigger value="plan">Plan</TabsTrigger>
          </TabsList>
        </Tabs>
        <Button
          variant="outline"
          size="sm"
          data-action="study-undo"
          disabled={!workspace?.read().canUndo || workspace.busy}
          onClick={() => run(() => workspace?.undo())}
        >
          Undo
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={!workspace?.read().canRedo || workspace.busy}
          onClick={() => run(() => workspace?.redo())}
        >
          Redo
        </Button>
        <Button variant="outline" size="sm" onClick={() => run(() => workspace?.fit())}>
          Fit view
        </Button>
        <Button
          variant="outline"
          size="sm"
          aria-expanded={controlsOpen}
          onClick={() => setControlsOpen(!controlsOpen)}
        >
          Controls
        </Button>
      </div>
      {error && (
        <p role="alert" className="shrink-0 px-5 py-2 text-sm text-destructive">
          {error}
        </p>
      )}
      <div className="flex min-h-0 flex-1">
        <div className="relative min-h-0 min-w-0 flex-1">
          <div
            ref={scene}
            data-study-scene
            className={`absolute inset-0 ${workspace?.view === 'plan' ? 'hidden' : ''}`}
          />
          <div
            ref={plan}
            data-study-plan
            className={`absolute inset-0 ${workspace ? (workspace.view === 'plan' ? '' : 'hidden') : 'invisible pointer-events-none'}`}
          />
        </div>
        {!compact && controlsOpen && (
          <aside
            className="w-80 shrink-0 overflow-auto border-l bg-card"
            aria-label="Study controls and report"
          >
            {controls}
          </aside>
        )}
      </div>
      {compact && (
        <Sheet open={controlsOpen} onOpenChange={setControlsOpen}>
          <SheetContent
            side="bottom"
            className="flex max-h-[85svh] flex-col overflow-hidden rounded-t-2xl bg-card p-0 shadow-none"
          >
            <SheetHeader className="shrink-0 border-b p-4 pr-12 text-left">
              <SheetTitle>Site controls</SheetTitle>
              <SheetDescription>Edit, review or compare designs.</SheetDescription>
            </SheetHeader>
            {workspace && <StudySummary workspace={workspace} compact />}
            <div className="min-h-0 overflow-auto">{controls}</div>
          </SheetContent>
        </Sheet>
      )}
      <p role="status" className="shrink-0 border-t px-3 py-2 text-xs text-muted-foreground">
        {workspace?.notice ?? 'Preparing site study…'}
      </p>
    </main>
  );
}

export function mountStudyPage(container: HTMLElement) {
  const root = createRoot(container);
  const ready = new Promise<SiteMassingWorkspace>((resolve, reject) => {
    root.render(<StudyPage ready={resolve} failed={reject} />);
  });
  return { ready, dispose: () => root.unmount() };
}
```

### examples/src/app/components/ui/button.tsx

```ts
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/utils';

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-normal transition-colors duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'bg-card text-foreground shadow-control hover:bg-accent',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-pebble',
        ghost: 'text-foreground hover:bg-accent hover:text-accent-foreground',
        link: 'text-foreground underline decoration-brand underline-offset-4 hover:decoration-foreground',
      },
      size: {
        default: 'h-9 px-3.5 py-2',
        sm: 'h-8 px-3 text-compact',
        lg: 'h-11 px-5 text-action',
        icon: 'h-9 w-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button';
    return (
      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
    );
  },
);
Button.displayName = 'Button';

export { Button, buttonVariants };
```

### examples/src/app/components/ui/tabs.tsx

```ts
'use client';

import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';

import { cn } from '@/lib/utils';

const Tabs = TabsPrimitive.Root;

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      'bg-muted inline-flex items-center gap-1 rounded-tabs p-1 text-muted-foreground',
      className,
    )}
    {...props}
  />
));
TabsList.displayName = TabsPrimitive.List.displayName;

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      'inline-flex h-9 items-center justify-center whitespace-nowrap rounded-tabs px-4 text-sm font-normal transition-colors duration-200 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-control data-[state=active]:hover:bg-card',
      className,
    )}
    {...props}
  />
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
      className,
    )}
    {...props}
  />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;

export { Tabs, TabsList, TabsTrigger, TabsContent };
```

### examples/src/app/components/ui/sheet.tsx

```ts
'use client';

import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { HugeiconsIcon } from '@hugeicons/react';
import { Cancel01Icon } from '@hugeicons/core-free-icons';

import { cn } from '@/lib/utils';

const Sheet = SheetPrimitive.Root;

const SheetTrigger = SheetPrimitive.Trigger;

const SheetClose = SheetPrimitive.Close;

const SheetPortal = SheetPrimitive.Portal;

const SheetOverlay = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Overlay
    className={cn(
      'fixed inset-0 z-50 bg-overlay/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
      className,
    )}
    {...props}
    ref={ref}
  />
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
  'fixed z-50 gap-4 bg-card p-7 transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
  {
    variants: {
      side: {
        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
        bottom:
          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
        left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
        right:
          'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
      },
    },
    defaultVariants: {
      side: 'right',
    },
  },
);

interface SheetContentProps
  extends
    React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
    VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Content>,
  SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
  <SheetPortal>
    <SheetOverlay />
    <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
      <SheetPrimitive.Close className="absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
        <HugeiconsIcon icon={Cancel01Icon} size={16} strokeWidth={1.5} />
        <span className="sr-only">Close</span>
      </SheetPrimitive.Close>
      {children}
    </SheetPrimitive.Content>
  </SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;

const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
  <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
);
SheetHeader.displayName = 'SheetHeader';

const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
    {...props}
  />
);
SheetFooter.displayName = 'SheetFooter';

const SheetTitle = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Title
    ref={ref}
    className={cn('text-subheading font-medium text-foreground', className)}
    {...props}
  />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;

const SheetDescription = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Description
    ref={ref}
    className={cn('text-sm text-muted-foreground', className)}
    {...props}
  />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;

export {
  Sheet,
  SheetPortal,
  SheetOverlay,
  SheetTrigger,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
};
```

### examples/src/app/hooks/use-mobile.tsx

```ts
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}
```

### examples/src/playbooks/site-massing-controls.tsx

```ts
import { Button } from '../app/components/ui/button';
import { Field, FieldLabel, FieldLegend, FieldSet } from '../app/components/ui/field';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../app/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../app/components/ui/tabs';
import { StudyResults } from './site-massing-results';
import { StudyNumberField } from './site-massing-number-field';
import { StudyOptions } from './site-massing-options';
import type { SiteMassingWorkspace } from './site-massing-workspace';

export type StudyPanel = 'edit' | 'results' | 'options';
interface StudyControlsProps {
  workspace: SiteMassingWorkspace;
  run: (action: () => unknown) => void;
  onDraw: () => void;
  panel: StudyPanel;
  onPanelChange: (panel: StudyPanel) => void;
  name: string;
  onNameChange: (name: string) => void;
}

export function StudyControls({
  workspace,
  run,
  onDraw,
  panel,
  onPanelChange,
  name,
  onNameChange,
}: StudyControlsProps) {
  const state = workspace.read();
  return (
    <Tabs
      value={panel}
      onValueChange={(value) => {
        if (value === 'edit' || value === 'results' || value === 'options') onPanelChange(value);
      }}
      data-study-controls
    >
      <div className="sticky top-0 z-10 border-b bg-card p-3">
        <TabsList aria-label="Study controls" className="flex w-full">
          <TabsTrigger value="edit" className="flex-1 px-2">
            Edit
          </TabsTrigger>
          <TabsTrigger value="results" className="flex-1 px-2">
            Results
          </TabsTrigger>
          <TabsTrigger value="options" className="flex-1 px-2">
            Options
          </TabsTrigger>
        </TabsList>
      </div>
      <TabsContent value="edit" className="mt-0">
        <FieldSet disabled={workspace.busy} className="gap-4 p-4">
          <FieldLegend variant="label" className="mb-0">
            Buildings
          </FieldLegend>
          <Field>
            <FieldLabel htmlFor="study-part">Building part</FieldLabel>
            <Select
              value={state.selected?.segmentId ?? ''}
              onValueChange={(id) => run(() => workspace.select(id))}
            >
              <SelectTrigger id="study-part">
                <SelectValue placeholder="Select a building part" />
              </SelectTrigger>
              <SelectContent>
                {state.parts.map((part) => (
                  <SelectItem key={part.segmentId} value={part.segmentId}>
                    {part.label}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </Field>
          {state.selected ? (
            <StudyNumberField
              key={state.selected.segmentId}
              label="Part height"
              value={state.selected.height}
              unit="m"
              commit={(height) => run(() => workspace.setHeight(height))}
            />
          ) : (
            <p className="text-xs text-muted-foreground">
              Select a part in the model to edit its height.
            </p>
          )}
          <div className="flex flex-wrap gap-2">
            <Button
              size="sm"
              onClick={() => run(() => workspace.addBuilding())}
              data-action="add-building"
            >
              Add second building
            </Button>
            <Button variant="outline" size="sm" onClick={onDraw} data-action="draw-building">
              Draw building
            </Button>
          </div>
          <p className="text-xs text-muted-foreground">
            Drag a corner to reshape. Delete removes the selected part.
          </p>
          <FieldSet className="gap-3 border-t pt-4">
            <FieldLegend variant="label">Site rules</FieldLegend>
            <StudyNumberField
              label="Front setback"
              value={state.frontSetback}
              unit="m"
              commit={(value) => run(() => workspace.setSetback(value))}
            />
            <Button
              variant="outline"
              size="sm"
              onClick={() => run(() => workspace.setEnvelope(!workspace.envelope))}
              aria-pressed={workspace.envelope}
              data-action="envelope"
            >
              Envelope {workspace.envelope ? 'on' : 'off'}
            </Button>
          </FieldSet>
        </FieldSet>
      </TabsContent>
      <TabsContent value="results" className="mt-0">
        <StudyResults workspace={workspace} />
      </TabsContent>
      <TabsContent value="options" className="mt-0">
        <StudyOptions workspace={workspace} run={run} name={name} onNameChange={onNameChange} />
      </TabsContent>
    </Tabs>
  );
}
```

### examples/src/playbooks/site-massing-summary.tsx

```ts
import { Badge } from '../app/components/ui/badge';
import { Card } from '../app/components/ui/card';
import type { SiteMassingWorkspace } from './site-massing-workspace';

interface StudySummaryProps {
  workspace: SiteMassingWorkspace;
  compact?: boolean;
}

export function StudySummary({ workspace, compact = false }: StudySummaryProps) {
  const { report, liveArea, coverageComplete } = workspace.read();
  const m = report.metrics;
  const rules = workspace.manager.session.snapshot().site?.zoning?.rules ?? [];
  const ratioCap = (quantity: 'floorAreaRatio' | 'siteCoverage') =>
    rules.reduce(
      (cap, rule) =>
        rule.kind === 'ratio-cap' && rule.quantity === quantity ? Math.min(cap, rule.max) : cap,
      Infinity,
    );
  const farCap = ratioCap('floorAreaRatio');
  const coverageCap = ratioCap('siteCoverage');
  const heightCap = rules.reduce(
    (cap, rule) => (rule.kind === 'height-cap' ? Math.min(cap, rule.max) : cap),
    Infinity,
  );
  const metrics = [
    {
      key: 'area',
      label: liveArea === undefined ? 'Floor area' : 'Floor area preview',
      value: `${(liveArea ?? m.grossFloorArea).toFixed(1)} m²`,
      detail: 'Across all floors',
    },
    {
      key: 'far',
      label: 'FAR',
      value: m.floorAreaRatio.toFixed(2),
      detail: Number.isFinite(farCap) ? `Limit ${farCap.toFixed(2)}` : 'No FAR limit supplied',
    },
    {
      key: 'coverage',
      label: 'Coverage',
      value: `${Number((m.siteCoverage * 100).toFixed(1))}%`,
      detail: Number.isFinite(coverageCap)
        ? `Limit ${coverageCap * 100}%`
        : 'No coverage limit supplied',
    },
    {
      key: 'height',
      label: 'Height',
      value: `${Number(m.maxHeight.toFixed(1))} m`,
      detail: Number.isFinite(heightCap) ? `Limit ${heightCap} m` : 'No height limit supplied',
    },
  ];
  return (
    <section aria-label="Study overview" data-study-summary className="shrink-0 border-b px-4 py-3">
      <div className="mb-3 flex items-center justify-between gap-3">
        <p className="text-xs text-muted-foreground">{m.siteArea} m² parcel · sample rules</p>
        <Badge
          variant={
            liveArea !== undefined || (report.compliant && coverageComplete)
              ? 'secondary'
              : 'destructive'
          }
          data-study-outcome
        >
          {liveArea !== undefined
            ? 'Release to check'
            : !coverageComplete
              ? 'Coverage needs review'
              : report.compliant
                ? 'Checks pass'
                : `${report.violations.length} ${report.violations.length === 1 ? 'issue' : 'issues'}`}
        </Badge>
      </div>
      <div className={`grid grid-cols-2 gap-2 ${compact ? '' : 'sm:grid-cols-4'}`}>
        {metrics
          .filter((metric) => !compact || metric.key === 'area' || metric.key === 'far')
          .map((metric) => (
            <Card
              key={metric.key}
              className="rounded-xl bg-muted px-3 py-2"
              data-study-metric={metric.key}
            >
              <p className="text-xs text-muted-foreground">{metric.label}</p>
              <p className="mt-1 text-subheading font-medium tabular-nums">{metric.value}</p>
              <p className="mt-1 text-xs text-muted-foreground">{metric.detail}</p>
            </Card>
          ))}
      </div>
    </section>
  );
}
```

### examples/src/playbooks/site-massing-workspace.ts

```ts
import {
  Building,
  GUIEditor,
  massSegmentHeightCommand,
  ProjectManager,
  type ManagedPlanEditor,
} from '@opengeometry/floorkit';
import { createScene, type SceneHandle } from '../app/lib/scene';
import {
  addSecondBuilding,
  captureOption,
  defaultAssumptions,
  openStudy,
  setFrontSetback,
  studyReport,
  type StudyAssumptions,
  type StudyOption,
} from './code/site-massing';

export interface StudyPart {
  buildingId: string;
  massId: string;
  segmentId: string;
  label: string;
  height: number;
}

export interface StudyContainers {
  scene: HTMLElement;
  plan: HTMLElement;
}

export interface SavedStudyOption extends StudyOption {
  coverageComplete: boolean;
}

/** Lifecycle of the two mounted SDK editors and the app's separate saved options. */
export class SiteMassingWorkspace {
  manager: ProjectManager;
  plan: ManagedPlanEditor | undefined;
  assumptions: StudyAssumptions = { ...defaultAssumptions };
  options: SavedStudyOption[] = [];
  envelope = false;
  busy = false;
  notice = 'Sample controls · not planning approval';
  view: 'plan' | '3d' = '3d';
  private scene: SceneHandle;
  private editor: GUIEditor;
  private stopSteps: () => void = () => undefined;
  private selectedMass = 'podium-tower-mass';
  private liveArea: number | undefined;
  private disposed = false;
  private readonly historyKey = (event: KeyboardEvent): void => {
    const typing =
      event.target instanceof HTMLElement &&
      (event.target.matches('input, textarea, select') ||
        event.target.isContentEditable ||
        event.target.closest('[role="combobox"], [role="listbox"]'));
    if (typing || !(event.ctrlKey || event.metaKey) || event.altKey) return;
    const key = event.key.toLowerCase();
    if (key !== 'z' && key !== 'y') return;
    event.preventDefault();
    event.stopImmediatePropagation();
    try {
      if (key === 'y' || event.shiftKey) this.manager.redo();
      else this.manager.undo();
    } catch (error) {
      this.showError(error);
    }
  };
  private readonly afterGesture = (event: Event): void => {
    if (event instanceof KeyboardEvent && event.key !== 'Escape') return;
    requestAnimationFrame(() => {
      if (this.disposed) return;
      this.liveArea = undefined;
      this.refresh();
    });
  };

  constructor(
    private readonly containers: StudyContainers,
    private readonly refresh: () => void,
  ) {
    this.manager = openStudy((error) => this.showError(error));
    this.manager.setBuildableEnvelope(false);
    this.scene = createScene(containers.scene, {
      camera: { position: [44, 35, 44], target: [15, 4, 10] },
      grid: false,
    });
    this.scene.scene.add(this.manager.root);
    this.editor = this.bindEditor();
    this.scene.onFrame(() => this.editor.update());
    for (const name of ['pointerup', 'pointercancel', 'keydown'])
      window.addEventListener(name, this.afterGesture, true);
    window.addEventListener('keydown', this.historyKey, true);
  }

  async mount(): Promise<void> {
    this.plan = await this.mountPlan(this.manager, this.containers.plan);
    this.watch();
    this.select('tower');
  }

  read() {
    const model = this.manager.session.snapshot();
    const parts: StudyPart[] = model.buildings.flatMap(
      (building) =>
        building.mass?.segments.map((segment) => ({
          buildingId: building.id,
          massId: building.mass?.id ?? '',
          segmentId: segment.id,
          label: `${segment.name ?? segment.id} · ${building.name ?? building.id}`,
          height: segment.height,
        })) ?? [],
    );
    const live = this.manager.session.currentBuild.elementsById.get(this.selectedMass);
    const selected =
      live instanceof Building
        ? parts.find((part) => part.massId === live.ogid && part.segmentId === live.activePart)
        : undefined;
    const report = studyReport(this.manager);
    const rule = model.site?.zoning?.rules.find(
      (item) => item.kind === 'envelope-plane' && item.edgeClass === 'front',
    );
    return {
      parts,
      selected,
      report,
      liveArea: this.liveArea,
      frontSetback: rule?.kind === 'envelope-plane' ? rule.offsetAtGrade : 0,
      canUndo: this.manager.session.canUndo,
      canRedo: this.manager.session.canRedo,
      grade: model.site?.elevation ?? 0,
      coverageComplete: model.buildings.every(
        (building) =>
          !building.mass ||
          Math.abs(building.mass.elevation - (model.site?.elevation ?? 0)) <= 0.01,
      ),
    };
  }

  select(segmentId: string): void {
    const part = this.read().parts.find((item) => item.segmentId === segmentId);
    if (!part) return;
    const live = this.manager.session.currentBuild.elementsById.get(part.massId);
    if (!(live instanceof Building)) return;
    this.selectedMass = part.massId;
    live.activePart = part.segmentId;
    if (this.view === '3d') this.editor.addControls(live);
    else this.plan?.renderer?.select(part.massId);
    this.refresh();
  }

  setView(view: 'plan' | '3d'): void {
    const selected = this.read().selected;
    this.editor.clearSelection();
    this.plan?.renderer?.select(undefined);
    this.view = view;
    this.refresh();
    requestAnimationFrame(() => {
      if (this.disposed) return;
      const renderer = this.plan?.renderer;
      if (view === 'plan' && renderer) {
        renderer.resize(this.containers.plan.clientWidth, this.containers.plan.clientHeight);
        renderer.fitToView();
      }
      if (selected) this.select(selected.segmentId);
    });
  }

  setHeight(height: number): void {
    const part = this.read().selected;
    if (!part) throw new Error('Select a mass part first.');
    const command = massSegmentHeightCommand(
      this.manager.session.snapshot(),
      part.buildingId,
      part.segmentId,
      height,
    );
    if (command) this.manager.apply(command);
  }

  setSetback(metres: number): void {
    setFrontSetback(this.manager, metres);
  }
  addBuilding(): void {
    addSecondBuilding(this.manager);
  }
  undo(): void {
    this.manager.undo();
  }
  redo(): void {
    this.manager.redo();
  }
  fit(): void {
    if (this.view === 'plan') this.plan?.renderer?.fitToView();
    else this.scene.fit();
  }
  setEnvelope(shown: boolean): void {
    this.envelope = shown;
    this.manager.setBuildableEnvelope(shown);
    this.refresh();
  }

  async draw(): Promise<void> {
    this.setView('plan');
    this.notice = 'Click corners; Enter finishes, Escape cancels.';
    this.refresh();
    const id = await this.plan?.startMassDrawing({
      storeyHeight: 3,
      storeyCount: 4,
      use: 'residential',
    });
    if (this.disposed) return;
    this.notice = id ? 'Building added as one undo step.' : 'Drawing cancelled; no building saved.';
    this.refresh();
  }

  saveOption(name: string): void {
    if (!name.trim()) throw new Error('Give the option a name.');
    this.options = [
      ...this.options,
      {
        ...captureOption(this.manager, name.trim(), this.assumptions),
        coverageComplete: this.read().coverageComplete,
      },
    ];
    this.notice = `Saved ${name.trim()}. Download options to keep them.`;
    this.refresh();
  }

  async reopen(option: StudyOption): Promise<void> {
    if (this.busy) return;
    this.busy = true;
    this.refresh();
    let next: ProjectManager;
    try {
      next = ProjectManager.open(option.projectText, { onError: (error) => this.showError(error) });
    } catch (error) {
      this.busy = false;
      this.refresh();
      throw error;
    }
    const host = document.createElement('div');
    host.className = 'absolute inset-0';
    this.containers.plan.append(host);
    try {
      next.setBuildableEnvelope(this.envelope);
      const plan = await this.mountPlan(next, host);
      if (this.disposed) {
        next.dispose();
        return;
      }
      this.stopSteps();
      this.editor.dispose();
      this.scene.scene.remove(this.manager.root);
      this.manager.dispose();
      this.containers.plan.replaceChildren(host);
      this.manager = next;
      this.plan = plan;
      this.scene.scene.add(next.root);
      this.editor = this.bindEditor();
      this.assumptions = { ...option.assumptions };
      this.selectedMass = '';
      this.watch();
      this.notice = `Reopened ${option.name}. Saved assumptions restored; undo history starts again.`;
    } catch (error) {
      next.dispose();
      host.remove();
      throw error;
    } finally {
      this.busy = false;
      this.refresh();
    }
  }

  dispose(): void {
    this.disposed = true;
    this.stopSteps();
    this.editor.dispose();
    this.scene.scene.remove(this.manager.root);
    this.manager.dispose();
    this.scene.dispose();
    for (const name of ['pointerup', 'pointercancel', 'keydown'])
      window.removeEventListener(name, this.afterGesture, true);
    window.removeEventListener('keydown', this.historyKey, true);
  }

  private watch(): void {
    this.stopSteps = this.manager.subscribe((change) => {
      this.liveArea = undefined;
      if (change.cause !== 'restore')
        this.notice =
          change.cause === 'undo'
            ? 'Undid last edit.'
            : change.cause === 'redo'
              ? 'Redid last edit.'
              : 'Edit saved.';
      this.refresh();
    });
  }
  private bindEditor(): GUIEditor {
    const editor = new GUIEditor(this.containers.scene, this.scene.camera, this.scene.controls);
    this.manager.bindSceneEditor(editor);
    editor.onSelect.add((target) => {
      this.selectedMass = target instanceof Building ? target.ogid : '';
      this.refresh();
    });
    editor.onDrag = () => this.preview();
    return editor;
  }
  private async mountPlan(
    manager: ProjectManager,
    container: HTMLElement,
  ): Promise<ManagedPlanEditor> {
    const plan = await manager.mountPlanEditor(container, 'site-plan');
    plan.onError = (error) => {
      this.notice = error instanceof Error ? error.message : String(error);
      this.refresh();
    };
    plan.renderer?.on('select', (id) => {
      this.selectedMass = id ?? '';
      this.refresh();
    });
    if (plan.renderer) plan.renderer.onDrag = () => this.preview();
    return plan;
  }
  private preview(): void {
    this.liveArea = [...this.manager.session.currentBuild.elementsById.values()].reduce(
      (sum, element) =>
        sum + (element instanceof Building ? element.quantities().grossFloorArea : 0),
      0,
    );
    this.refresh();
  }
  private showError(error: unknown): void {
    this.notice = error instanceof Error ? error.message : String(error);
    this.refresh();
  }
}
```

### examples/src/app/components/ui/field.tsx

```ts
"use client"

import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"

function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
  return (
    <fieldset
      data-slot="field-set"
      className={cn(
        "flex flex-col gap-6",
        "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
        className
      )}
      {...props}
    />
  )
}

function FieldLegend({
  className,
  variant = "legend",
  ...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
  return (
    <legend
      data-slot="field-legend"
      data-variant={variant}
      className={cn(
        "mb-3 font-medium",
        "data-[variant=legend]:text-body",
        "data-[variant=label]:text-sm",
        className
      )}
      {...props}
    />
  )
}

function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-group"
      className={cn(
        "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
        className
      )}
      {...props}
    />
  )
}

const fieldVariants = cva(
  "group/field data-[invalid=true]:text-destructive flex w-full gap-3",
  {
    variants: {
      orientation: {
        vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
        horizontal: [
          "flex-row items-center",
          "[&>[data-slot=field-label]]:flex-auto",
          "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
        ],
        responsive: [
          "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
          "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
          "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
        ],
      },
    },
    defaultVariants: {
      orientation: "vertical",
    },
  }
)

function Field({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
  return (
    <div
      role="group"
      data-slot="field"
      data-orientation={orientation}
      className={cn(fieldVariants({ orientation }), className)}
      {...props}
    />
  )
}

function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-content"
      className={cn(
        "group/field-content flex flex-1 flex-col gap-1.5",
        className
      )}
      {...props}
    />
  )
}

function FieldLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  return (
    <Label
      data-slot="field-label"
      className={cn(
        "group/field-label peer/field-label flex w-fit gap-2 group-data-[disabled=true]/field:opacity-50",
        "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
        "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
        className
      )}
      {...props}
    />
  )
}

function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-label"
      className={cn(
        "flex w-fit items-center gap-2 text-label font-medium group-data-[disabled=true]/field:opacity-50",
        className
      )}
      {...props}
    />
  )
}

function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="field-description"
      className={cn(
        "text-muted-foreground text-sm font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
        "[&:nth-last-child(2)]:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
        "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
        className
      )}
      {...props}
    />
  )
}

function FieldSeparator({
  children,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  children?: React.ReactNode
}) {
  return (
    <div
      data-slot="field-separator"
      data-content={!!children}
      className={cn(
        "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
        className
      )}
      {...props}
    >
      <Separator className="absolute inset-0 top-1/2" />
      {children && (
        <span
          className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
          data-slot="field-separator-content"
        >
          {children}
        </span>
      )}
    </div>
  )
}

function FieldError({
  className,
  children,
  errors,
  ...props
}: React.ComponentProps<"div"> & {
  errors?: Array<{ message?: string } | undefined>
}) {
  const content = useMemo(() => {
    if (children) {
      return children
    }

    if (!errors) {
      return null
    }

    if (errors?.length === 1 && errors[0]?.message) {
      return errors[0].message
    }

    return (
      <ul className="ml-4 flex list-disc flex-col gap-1">
        {errors.map(
          (error, index) =>
            error?.message && <li key={index}>{error.message}</li>
        )}
      </ul>
    )
  }, [children, errors])

  if (!content) {
    return null
  }

  return (
    <div
      role="alert"
      data-slot="field-error"
      className={cn("text-destructive text-sm font-normal", className)}
      {...props}
    >
      {content}
    </div>
  )
}

export {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldContent,
  FieldTitle,
}
```

### examples/src/app/components/ui/select.tsx

```ts
'use client';

import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { HugeiconsIcon } from '@hugeicons/react';
import { ArrowDown01Icon, ArrowUp01Icon, Tick02Icon } from '@hugeicons/core-free-icons';

import { cn } from '@/lib/utils';

// shadcn Radix Select composition, adapted to the shared Tailwind 3 theme and icons.
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    data-slot="select-trigger"
    className={cn(
      'flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-input bg-card px-3 text-field font-medium text-foreground outline-none transition-[color,box-shadow] data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-destructive disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0',
      className,
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <HugeiconsIcon
        icon={ArrowDown01Icon}
        strokeWidth={1.5}
        className="size-4 opacity-50"
        aria-hidden="true"
      />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn('flex cursor-default items-center justify-center py-1', className)}
    {...props}
  >
    <HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={1.5} className="size-4" aria-hidden="true" />
  </SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn('flex cursor-default items-center justify-center py-1', className)}
    {...props}
  >
    <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={1.5} className="size-4" aria-hidden="true" />
  </SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(
  (
    { className, children, position = 'popper', align = 'start', sideOffset = 4, ...props },
    ref,
  ) => (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        ref={ref}
        data-slot="select-content"
        className={cn(
          'relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-hidden rounded-popover bg-popover text-popover-foreground shadow-popover origin-[var(--radix-select-content-transform-origin)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
          className,
        )}
        position={position}
        align={align}
        sideOffset={sideOffset}
        {...props}
      >
        <SelectScrollUpButton />
        <SelectPrimitive.Viewport
          className={cn(
            'p-0',
            position === 'popper' &&
              'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        <SelectScrollDownButton />
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  ),
);
SelectContent.displayName = SelectPrimitive.Content.displayName;

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn('px-2.5 py-1.5 text-xs text-muted-foreground', className)}
    {...props}
  />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    data-value={props.value}
    data-slot="select-item"
    className={cn(
      'relative flex min-h-9 w-full cursor-default select-none items-center gap-2 rounded-none px-2.5 py-0 pr-8 text-field font-normal outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
      className,
    )}
    {...props}
  >
    <span className="absolute right-2.5 flex size-4 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <HugeiconsIcon icon={Tick02Icon} strokeWidth={1.5} className="size-4" aria-hidden="true" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn('pointer-events-none my-1 h-px bg-border', className)}
    {...props}
  />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;

export {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectGroup,
  SelectLabel,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
};
```

### examples/src/playbooks/site-massing-results.tsx

```ts
import { Card } from '../app/components/ui/card';
import { Button } from '../app/components/ui/button';
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '../app/components/ui/collapsible';
import { ReportPanel } from '../app/components/shell/ReportPanel';
import { studySections, violationLabel } from './site-massing-report';
import type { SiteMassingWorkspace } from './site-massing-workspace';

interface StudyResultsProps {
  workspace: SiteMassingWorkspace;
}

export function StudyResults({ workspace }: StudyResultsProps) {
  const { report } = workspace.read();
  const model = workspace.manager.session.snapshot();
  const nameOf = (id: string) => {
    const building = model.buildings.find((entry) => entry.id === id || entry.mass?.id === id);
    return building?.name ?? id;
  };
  return (
    <div>
      <section
        data-report-section="Violations"
        className="flex flex-col gap-3 border-b p-4"
        aria-label="Planning checks"
      >
        <h3 className="text-sm font-medium">Planning checks</h3>
        {report.violations.length === 0 && (
          <p className="text-sm text-muted-foreground">No issues with the supplied rules.</p>
        )}
        {report.violations.map((issue, index) => {
          const edge =
            issue.edgeIndex === undefined ? undefined : model.site?.lotLines?.[issue.edgeIndex];
          const affected = issue.massId
            ? nameOf(issue.massId)
            : (issue.masses?.map(nameOf).join(' and ') ?? 'Site');
          const percent = issue.type === 'exceeds-lot-coverage';
          const unit = percent
            ? '%'
            : issue.type === 'exceeds-far'
              ? ''
              : issue.type === 'exceeds-volume-ratio'
                ? ' m³/m²'
                : ' m';
          const value = (number: number) =>
            `${Number((number * (percent ? 100 : 1)).toFixed(2))}${unit}`;
          return (
            <Card
              key={index}
              data-rule-type={issue.type}
              data-edge-index={issue.edgeIndex}
              className="rounded-xl border p-3"
            >
              <p className="text-sm font-medium">{violationLabel(issue.type)}</p>
              <p className="mt-1 text-xs text-muted-foreground">
                {affected}
                {issue.edgeIndex === undefined ? '' : ` · ${edge ?? 'parcel'} edge`}
              </p>
              {issue.current !== undefined && (
                <p className="mt-2 text-sm tabular-nums">
                  Current {value(issue.current)}
                  {issue.limit === undefined ? '' : ` · limit ${value(issue.limit)}`}
                </p>
              )}
              <Collapsible className="mt-2">
                <CollapsibleTrigger asChild>
                  <Button variant="ghost" size="sm">
                    Details
                  </Button>
                </CollapsibleTrigger>
                <CollapsibleContent className="space-y-2 pt-2 text-xs text-muted-foreground">
                  <p>{issue.message}</p>
                  <p className="break-words">Rule: {issue.ruleId ?? issue.type}</p>
                  {issue.edgeIndex !== undefined && <p>Edge index: {issue.edgeIndex}</p>}
                </CollapsibleContent>
              </Collapsible>
            </Card>
          );
        })}
      </section>
      <ReportPanel sections={studySections(workspace)} />
    </div>
  );
}
```

### examples/src/playbooks/site-massing-number-field.tsx

```ts
import { InspectorNumberField } from '../app/components/shell/inspector-number-field';

interface StudyNumberFieldProps {
  label: string;
  value: number;
  unit: string;
  commit: (value: number) => void;
}

export function StudyNumberField({ label, value, unit, commit }: StudyNumberFieldProps) {
  return (
    <InspectorNumberField
      id={`study-${label.replaceAll(' ', '-').toLowerCase()}`}
      row={{
        kind: 'value',
        key: label,
        label,
        group: 'Study',
        scope: 'study',
        control: { kind: 'number', unit },
        read: () => value,
      }}
      apply={(_row, candidate) => {
        if (typeof candidate === 'number') commit(candidate);
      }}
    />
  );
}
```

### examples/src/playbooks/site-massing-options.tsx

```ts
import { Button } from '../app/components/ui/button';
import { Card } from '../app/components/ui/card';
import { Input } from '../app/components/ui/input';
import { Field, FieldLabel, FieldLegend, FieldSet } from '../app/components/ui/field';
import { residentialEstimate } from './code/site-massing';
import { StudyNumberField } from './site-massing-number-field';
import { StudyDownload } from './site-massing-download';
import { violationLabel } from './site-massing-report';
import type { SiteMassingWorkspace } from './site-massing-workspace';

interface StudyOptionsProps {
  workspace: SiteMassingWorkspace;
  run: (action: () => unknown) => void;
  name: string;
  onNameChange: (name: string) => void;
}

export function StudyOptions({ workspace, run, name, onNameChange }: StudyOptionsProps) {
  const basis = JSON.stringify(workspace.manager.session.snapshot().site);
  return (
    <FieldSet disabled={workspace.busy} className="gap-5 p-4">
      <FieldLegend variant="label" className="mb-0">
        Compare designs
      </FieldLegend>
      <Field>
        <FieldLabel htmlFor="study-option-name">Option name</FieldLabel>
        <Input
          id="study-option-name"
          value={name}
          onChange={(event) => onNameChange(event.target.value)}
        />
      </Field>
      <Button onClick={() => run(() => workspace.saveOption(name))} data-action="save-option">
        Save option
      </Button>
      <div data-report-section="Saved options" className="flex flex-col gap-3">
        {workspace.options.length === 0 && (
          <p className="text-sm text-muted-foreground">
            Save this design, make a change, then save another to compare.
          </p>
        )}
        {workspace.options.map((option, index) => {
          const m = option.report.metrics;
          const estimate = residentialEstimate(option.report, option.assumptions);
          return (
            <Card key={index} className="rounded-xl border p-3">
              <div className="flex items-center justify-between gap-2">
                <p className="min-w-0 break-words text-sm font-medium">{option.name}</p>
                <Button
                  variant="ghost"
                  size="sm"
                  aria-label={`Reopen ${option.name}`}
                  onClick={() => run(() => workspace.reopen(option))}
                >
                  Reopen
                </Button>
              </div>
              <p className="mt-2 text-subheading font-medium tabular-nums">{m.grossFloorArea} m²</p>
              <p className="mt-1 text-xs text-muted-foreground">
                FAR {m.floorAreaRatio.toFixed(2)} · {(m.siteCoverage * 100).toFixed(0)}% coverage ·{' '}
                {m.maxHeight} m high
              </p>
              <p className="mt-2 text-xs">
                {option.siteBasis === basis ? 'Same site and rules' : 'Different site or rules'} ·{' '}
                {!option.coverageComplete
                  ? 'Coverage needs review'
                  : option.report.compliant
                    ? 'Checks pass'
                    : `${option.report.violations.length} issue(s)`}
              </p>
              {option.report.violations.length > 0 && (
                <p className="mt-1 break-words text-xs text-muted-foreground">
                  {option.report.violations.map((v) => violationLabel(v.type)).join(', ')}
                </p>
              )}
              <p className="mt-2 text-xs text-muted-foreground">
                {estimate.estimatedUnits} estimated homes ·{' '}
                {option.assumptions.residentialEfficiency * 100}% efficiency ·{' '}
                {option.assumptions.averageUnitArea} m² each
              </p>
            </Card>
          );
        })}
      </div>
      <FieldSet className="gap-3 border-t pt-4">
        <FieldLegend variant="label">Home estimate</FieldLegend>
        <StudyNumberField
          label="Usable share"
          value={workspace.assumptions.residentialEfficiency * 100}
          unit="%"
          commit={(value) =>
            run(() => {
              if (value <= 0 || value > 100)
                throw new Error('Efficiency must be above 0 and at most 100%.');
              workspace.assumptions = {
                ...workspace.assumptions,
                residentialEfficiency: value / 100,
              };
            })
          }
        />
        <StudyNumberField
          label="Average home area"
          value={workspace.assumptions.averageUnitArea}
          unit="m²"
          commit={(value) =>
            run(() => {
              if (value <= 0) throw new Error('Average home area must be positive.');
              workspace.assumptions = { ...workspace.assumptions, averageUnitArea: value };
            })
          }
        />
        <p className="text-xs text-muted-foreground">
          An area estimate; room layouts and parking fit are not checked.
        </p>
      </FieldSet>
      <div className="border-t pt-4">
        <StudyDownload workspace={workspace} run={run} />
      </div>
      <p className="text-xs text-muted-foreground">
        Options stay in this session. Download them before closing.
      </p>
    </FieldSet>
  );
}
```

### examples/src/app/components/ui/badge.tsx

```ts
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/utils';

// Keep status labels quiet; reserve brand colour for explicitly branded badges.
const badgeVariants = cva(
  'inline-flex items-center gap-1 rounded-none px-0 py-1 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
  {
    variants: {
      variant: {
        default: 'text-foreground',
        secondary: 'text-muted-foreground',
        destructive: 'text-destructive',
        outline: 'rounded-full border border-border px-2.5 text-foreground',
        brand: 'text-foreground border-b border-brand',
      },
    },
    defaultVariants: {
      variant: 'default',
    },
  },
);

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}

export { Badge, badgeVariants };
```

### examples/src/app/components/ui/card.tsx

```ts
import * as React from 'react';

import { cn } from '@/lib/utils';

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('rounded-2xl bg-card text-card-foreground', className)}
      {...props}
    />
  ),
);
Card.displayName = 'Card';

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('flex flex-col space-y-2 p-7', className)} {...props} />
  ),
);
CardHeader.displayName = 'CardHeader';

const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('text-subheading font-medium leading-none', className)}
      {...props}
    />
  ),
);
CardTitle.displayName = 'CardTitle';

const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('text-body text-muted-foreground', className)} {...props} />
  ),
);
CardDescription.displayName = 'CardDescription';

const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('p-7 pt-0', className)} {...props} />
  ),
);
CardContent.displayName = 'CardContent';

const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('flex items-center p-7 pt-0', className)} {...props} />
  ),
);
CardFooter.displayName = 'CardFooter';

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
```

### examples/src/app/lib/scene.ts

```ts
import * as THREE from 'three';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { fitScene } from './fit-scene';

export interface SceneHandle {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGLRenderer;
  controls: OrbitControls;
  onFrame: (callback: () => void) => void;
  fit: () => void;
  dispose: () => void;
}

export interface SceneOptions {
  camera: { position: [number, number, number]; target: [number, number, number] };
  grid?: false | { size: number; y?: number };
}

export function createScene(
  container: HTMLElement,
  { camera, grid = { size: 40 } }: SceneOptions,
): SceneHandle {
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0xfbfaf9);
  const perspective = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
  perspective.position.set(...camera.position);
  const renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.0;
  const pmrem = new THREE.PMREMGenerator(renderer);
  const environment = pmrem.fromScene(new RoomEnvironment(), 0.04);
  scene.environment = environment.texture;
  container.appendChild(renderer.domElement);
  const controls = new OrbitControls(perspective, renderer.domElement);
  controls.target.set(...camera.target);
  controls.update();
  scene.add(new THREE.HemisphereLight(0xffffff, 0x888888, 1.1));
  const sun = new THREE.DirectionalLight(0xffffff, 1.3);
  sun.position.set(6, 10, 4);
  scene.add(sun);
  if (grid) {
    const helper = new THREE.GridHelper(grid.size, grid.size, 0xbab9b8, 0xe4e3e1);
    helper.position.y = grid.y ?? 0;
    scene.add(helper);
  }

  let interacted = false;
  const preserveView = () => {
    interacted = true;
  };
  renderer.domElement.addEventListener('pointerdown', preserveView);
  renderer.domElement.addEventListener('wheel', preserveView, { passive: true });
  const fit = () => fitScene(scene, perspective, controls);
  const resize = () => {
    const { clientWidth, clientHeight } = container;
    if (!clientWidth || !clientHeight) return;
    perspective.aspect = clientWidth / clientHeight;
    perspective.updateProjectionMatrix();
    renderer.setSize(clientWidth, clientHeight);
    if (!interacted && perspective.aspect < 1) fit();
  };
  resize();
  const observer = new ResizeObserver(resize);
  observer.observe(container);

  const frameCallbacks: Array<() => void> = [];
  let frame = 0;
  const animate = () => {
    frame = requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, perspective);
    for (const callback of frameCallbacks) callback();
  };
  animate();

  return {
    scene,
    camera: perspective,
    renderer,
    controls,
    onFrame: (callback) => frameCallbacks.push(callback),
    fit,
    dispose: () => {
      cancelAnimationFrame(frame);
      observer.disconnect();
      renderer.domElement.removeEventListener('pointerdown', preserveView);
      renderer.domElement.removeEventListener('wheel', preserveView);
      controls.dispose();
      environment.dispose();
      pmrem.dispose();
      renderer.dispose();
      renderer.forceContextLoss();
      renderer.domElement.remove();
    },
  };
}
```

### examples/src/playbooks/code/site-massing.ts

```ts
import {
  ProjectManager,
  basicZoningRules,
  evaluateZoning,
  irMassToMassLike,
  massBuildingCommand,
  massSegmentHeightCommand,
  type BuildingModel,
  type ManagedPlanEditor,
  type ProjectFile,
  type ZoningReport,
} from '@opengeometry/floorkit';

export const siteModel: BuildingModel = {
  schema: 'floorkit.building-model',
  version: 3,
  units: 'meters',
  upAxis: 'y',
  id: 'site-study',
  site: {
    id: 'parcel',
    boundary: {
      outerRing: [
        [0, 0],
        [30, 0],
        [30, 20],
        [0, 20],
      ],
    },
    lotLines: ['front', 'side', 'rear', 'side'],
    elevation: 0,
    trueNorthDeg: 0,
    zoning: basicZoningRules({
      id: 'study-controls',
      maxFAR: 0.7,
      maxCoverage: 0.4,
      maxHeight: 15,
      setbacks: { front: 3, side: 3, rear: 3 },
      minSeparation: 4,
      countsTowardFAR: ['gross'],
    }),
  },
  buildings: [
    {
      id: 'podium-tower',
      name: 'Podium and tower',
      storeys: [
        { id: 'ground', elevation: 0, height: 4, usage: 'retail' },
        { id: 'first', elevation: 4, height: 3, usage: 'residential' },
        { id: 'second', elevation: 7, height: 3, usage: 'residential' },
      ],
      mass: {
        id: 'podium-tower-mass',
        elevation: 0,
        segments: [
          {
            id: 'podium',
            name: 'Podium',
            footprint: {
              outerRing: [
                [4, 4],
                [16, 4],
                [16, 12],
                [4, 12],
              ],
            },
            height: 4,
            use: 'retail',
            storeyIds: ['ground'],
          },
          {
            id: 'tower',
            name: 'Tower',
            footprint: {
              outerRing: [
                [6, 5],
                [14, 5],
                [14, 11],
                [6, 11],
              ],
            },
            height: 6,
            use: 'residential',
            storeyIds: ['first', 'second'],
          },
        ],
      },
    },
  ],
};

export const siteProject: ProjectFile = {
  schema: 'floorkit.project',
  version: 3,
  buildingModel: siteModel,
  views: [
    {
      id: 'site-plan',
      name: 'Site massing',
      kind: 'plan',
      storeyId: 'ground',
      cutHeightAboveStorey: 1.2,
      showMassing: true,
    },
  ],
  paper: {
    version: 1,
    unitSystem: 'metric',
    sheets: [
      {
        id: 'site-sheet',
        name: 'Site study',
        number: 'S-01',
        page: {
          format: 'A3',
          orientation: 'landscape',
          size: { width: 420, height: 297 },
          margin: 10,
        },
        viewports: [
          {
            id: 'site-viewport',
            viewId: 'site-plan',
            drawingNumber: '1',
            frame: { x: 20, y: 20, width: 380, height: 245 },
            scale: 0.01,
            rotationDeg: 0,
            clip: true,
            title: { text: 'Site massing', showScale: true },
          },
        ],
        annotations: [],
        titleblock: { templateId: 'default', values: { sheetNumber: 'S-01' } },
      },
    ],
  },
};

export function openStudy(onError?: (error: unknown) => void): ProjectManager {
  const manager = new ProjectManager(siteProject, { onError });
  manager.setBuildableEnvelope(true);
  return manager;
}

export async function mountSitePlan(
  manager: ProjectManager,
  container: HTMLElement,
): Promise<ManagedPlanEditor> {
  return manager.mountPlanEditor(container, 'site-plan');
}

export function addSecondBuilding(manager: ProjectManager): void {
  const command = massBuildingCommand(
    manager.session.snapshot(),
    [
      [20, 4],
      [26, 4],
      [26, 12],
      [20, 12],
    ],
    { name: 'Second building', storeyHeight: 3, storeyCount: 4, use: 'residential' },
  );
  manager.apply(command);
}

export async function drawBuilding(plan: ManagedPlanEditor): Promise<string | undefined> {
  return plan.startMassDrawing({ storeyHeight: 3, storeyCount: 4, use: 'residential' });
}

export function raiseTower(manager: ProjectManager, height: number): void {
  const command = massSegmentHeightCommand(
    manager.session.snapshot(),
    'podium-tower',
    'tower',
    height,
  );
  if (command) manager.apply(command);
}

export function studyReport(manager: ProjectManager): ZoningReport {
  const { site, buildings } = manager.session.snapshot();
  if (!site?.boundary || !site.zoning) throw new Error('The study needs a parcel and controls.');
  const masses = buildings.flatMap((building) =>
    building.mass ? [irMassToMassLike(building)] : [],
  );
  return evaluateZoning(
    { profile: site.boundary, lotLines: site.lotLines, elevation: site.elevation },
    masses,
    site.zoning,
  );
}

export function watchStudy(
  manager: ProjectManager,
  render: (report: ZoningReport) => void,
): () => void {
  render(studyReport(manager));
  return manager.subscribe(() => render(studyReport(manager)));
}

export function setFrontSetback(manager: ProjectManager, metres: number): void {
  const site = manager.session.snapshot().site;
  if (!site?.zoning) throw new Error('The study needs zoning controls.');
  const rules = site.zoning.rules.map((rule) =>
    rule.kind === 'envelope-plane' && rule.edgeClass === 'front'
      ? { ...rule, offsetAtGrade: metres }
      : rule,
  );
  manager.apply({ kind: 'replace-site', site: { ...site, zoning: { ...site.zoning, rules } } });
}

export interface StudyAssumptions {
  residentialEfficiency: number;
  averageUnitArea: number;
}

export const defaultAssumptions: StudyAssumptions = {
  residentialEfficiency: 0.8,
  averageUnitArea: 60,
};

export function residentialEstimate(report: ZoningReport, assumptions = defaultAssumptions) {
  const grossResidentialArea = report.metrics.perMass.reduce(
    (sum, mass) => sum + (mass.grossFloorAreaByUse.residential ?? 0),
    0,
  );
  if (
    !Number.isFinite(assumptions.residentialEfficiency) ||
    assumptions.residentialEfficiency <= 0 ||
    assumptions.residentialEfficiency > 1 ||
    !Number.isFinite(assumptions.averageUnitArea) ||
    assumptions.averageUnitArea <= 0
  )
    throw new Error(
      'Efficiency must be above 0 and at most 1; average unit area must be positive.',
    );
  const estimatedUsableArea = grossResidentialArea * assumptions.residentialEfficiency;
  return {
    grossResidentialArea,
    estimatedUsableArea,
    estimatedUnits: Math.floor(estimatedUsableArea / assumptions.averageUnitArea),
  };
}

export interface StudyOption {
  name: string;
  projectText: string;
  assumptions: StudyAssumptions;
  siteBasis: string;
  report: ZoningReport;
}

export function captureOption(
  manager: ProjectManager,
  name: string,
  assumptions: StudyAssumptions,
): StudyOption {
  const report = studyReport(manager);
  residentialEstimate(report, assumptions);
  return {
    name,
    projectText: manager.save(),
    assumptions: { ...assumptions },
    siteBasis: JSON.stringify(manager.session.snapshot().site),
    report,
  };
}
```

### examples/src/app/components/ui/collapsible.tsx

```ts
"use client"

import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"

const Collapsible = CollapsiblePrimitive.Root

const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger

const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent

export { Collapsible, CollapsibleTrigger, CollapsibleContent }
```

### examples/src/app/components/shell/ReportPanel.tsx

```ts
import { Fragment } from 'react';

import { FieldLegend, FieldSet } from '@/components/ui/field';
import { Badge } from '@/components/ui/badge';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import type { ReportSection, Verdict } from '@/lib/example';

function VerdictBadge({ verdict }: { verdict: Verdict }) {
  return verdict === 'pass' ? (
    <Badge variant="outline" data-verdict="pass">
      Pass
    </Badge>
  ) : (
    <Badge variant="default" data-verdict="fail">
      Fail
    </Badge>
  );
}

function Breakable({ value }: { value: string | number }) {
  if (typeof value === 'number') return <>{value}</>;
  return (
    <>
      {value.split(/(?<=[_/.])/).map((part, index) => (
        <Fragment key={index}>
          {index ? <wbr /> : null}
          {part}
        </Fragment>
      ))}
    </>
  );
}

export function ReportPanel({ sections }: { sections: readonly ReportSection[] }) {
  return (
    <div className="flex flex-col gap-5 p-5" data-report>
      {sections.map((section, sectionIndex) => {
        const hasVerdicts = section.rows?.some((row) => row.verdict);
        return (
          <FieldSet
            key={`${sectionIndex}:${section.title}`}
            className="gap-5"
            data-report-section={section.title}
          >
            <FieldLegend
              variant="label"
              className="mb-0 text-xs leading-[1.33] text-muted-foreground"
            >
              {section.title}
            </FieldLegend>
            {section.verdict ? <VerdictBadge verdict={section.verdict} /> : null}
            {section.rows?.length ? (
              <Table>
                {section.columns ? (
                  <TableHeader>
                    <TableRow>
                      {section.columns.map((column) => (
                        <TableHead key={column}>{column}</TableHead>
                      ))}
                      {hasVerdicts ? <TableHead /> : null}
                    </TableRow>
                  </TableHeader>
                ) : null}
                <TableBody>
                  {section.rows.map((row, index) => (
                    <TableRow key={index}>
                      {row.cells.map((cell, column) => (
                        <TableCell key={column} className="text-xs tabular-nums">
                          <Breakable value={cell} />
                        </TableCell>
                      ))}
                      {hasVerdicts ? (
                        <TableCell className="w-px whitespace-nowrap text-right">
                          {row.verdict ? <VerdictBadge verdict={row.verdict} /> : null}
                        </TableCell>
                      ) : null}
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            ) : null}
            {section.note ? <p className="text-sm text-muted-foreground">{section.note}</p> : null}
          </FieldSet>
        );
      })}
    </div>
  );
}
```

### examples/src/playbooks/site-massing-report.ts

```ts
import type { ZoningViolation } from '@opengeometry/floorkit';
import type { ReportSection } from '../app/lib/example';
import { residentialEstimate } from './code/site-massing';
import type { SiteMassingWorkspace } from './site-massing-workspace';

const violationNames: Record<ZoningViolation['type'], string> = {
  'exceeds-far': 'Too much counted floor area',
  'exceeds-lot-coverage': 'Too much ground coverage',
  'exceeds-volume-ratio': 'Too much building volume',
  'exceeds-height': 'Building is too tall',
  'setback-violation': 'Building is inside a setback',
  'front-setback-violation': 'Building is inside the front setback',
  'envelope-violation': 'Building crosses the envelope',
  'outside-parcel': 'Building crosses the parcel boundary',
  'building-spacing': 'Buildings are too close',
  'building-collision': 'Buildings overlap',
  'missing-zoning-basis': 'The rule basis is incomplete',
};

export function violationLabel(type: ZoningViolation['type']): string {
  return violationNames[type];
}

export function studySections(workspace: SiteMassingWorkspace): ReportSection[] {
  const { report, grade, coverageComplete } = workspace.read();
  const estimate = residentialEstimate(report, workspace.assumptions);
  return [
    {
      title: 'Area basis',
      columns: ['Measure', 'Value'],
      rows: [
        { cells: ['Parcel area', `${report.metrics.siteArea} m²`] },
        { cells: ['Ground level', `${grade} m`] },
        {
          cells: ['Floor area counted for FAR', `${report.metrics.countedFloorArea.toFixed(1)} m²`],
        },
      ],
      note: coverageComplete
        ? 'Sample rules, not planning approval.'
        : 'Check coverage separately: a mass starts above or below ground level and is excluded from the subtotal.',
    },
    {
      title: 'Home estimate',
      columns: ['Measure', 'Value'],
      rows: [
        { cells: ['Residential floor area', `${estimate.grossResidentialArea.toFixed(1)} m²`] },
        { cells: ['Estimated usable area', `${estimate.estimatedUsableArea.toFixed(1)} m²`] },
        { cells: ['Estimated homes', estimate.estimatedUnits] },
      ],
      note: `${workspace.assumptions.residentialEfficiency * 100}% efficiency · ${workspace.assumptions.averageUnitArea} m² per home. Layouts and parking fit are not checked.`,
    },
  ];
}
```

### examples/src/app/components/shell/inspector-number-field.tsx

```ts
import { useRef, useState } from 'react';

import { Field, FieldLabel } from '@/components/ui/field';
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from '@/components/ui/input-group';
import type { ValueRow } from '@/lib/schema';

import { numericComponents, parseNumberDraft } from './inspector-number';

interface InspectorNumberFieldProps {
  row: ValueRow;
  id: string;
  apply(row: ValueRow, value: unknown, commit: boolean): void;
}

export function InspectorNumberField({ row, id, apply }: InspectorNumberFieldProps) {
  const size = row.control.kind === 'vec2' ? 2 : row.control.kind === 'vec3' ? 3 : 1;
  const axes = size === 1 ? [''] : size === 2 ? ['X', 'Y'] : ['X', 'Y', 'Z'];
  return (
    <Field data-control={row.key}>
      <FieldLabel htmlFor={id}>{row.label}</FieldLabel>
      <div className={size === 1 ? '' : 'grid grid-cols-2 gap-2'}>
        {axes.map((axis, index) => (
          <NumberAxis
            key={axis}
            row={row}
            id={index === 0 ? id : `${id}-${axis.toLowerCase()}`}
            axis={axis}
            index={index}
            size={size}
            apply={apply}
          />
        ))}
      </div>
    </Field>
  );
}

interface NumberAxisProps extends InspectorNumberFieldProps {
  axis: string;
  index: number;
  size: number;
}

function NumberAxis({ row, id, axis, index, size, apply }: NumberAxisProps) {
  const [draft, setDraft] = useState<string>();
  const skipBlur = useRef(false);
  const current = numericComponents(row.read(), size)[index];
  const commit = () => {
    if (draft === undefined) return;
    const next = parseNumberDraft(draft);
    setDraft(undefined);
    if (next === undefined) return;
    const values = numericComponents(row.read(), size);
    if (size === 1) {
      if (next !== values[0]) apply(row, next, true);
      return;
    }
    const candidate = values.map((component, position) => (position === index ? next : component));
    if (!candidate.every((component): component is number => component !== undefined)) return;
    if (next !== values[index]) apply(row, candidate, true);
  };
  return (
    <InputGroup>
      {axis ? (
        <InputGroupAddon>
          <InputGroupText>{axis}</InputGroupText>
        </InputGroupAddon>
      ) : null}
      <InputGroupInput
        id={id}
        type="text"
        inputMode="decimal"
        aria-label={axis ? `${row.label} ${axis} axis` : row.label}
        className="tabular-nums"
        value={draft ?? (current === undefined ? '' : String(current))}
        onChange={(event) => setDraft(event.target.value)}
        onBlur={() => {
          if (skipBlur.current) {
            skipBlur.current = false;
            return;
          }
          commit();
        }}
        onKeyDown={(event) => {
          if (event.key !== 'Enter' && event.key !== 'Escape') return;
          event.preventDefault();
          event.stopPropagation();
          skipBlur.current = true;
          if (event.key === 'Enter') commit();
          else setDraft(undefined);
          event.currentTarget.blur();
        }}
      />
      {'unit' in row.control && row.control.unit ? (
        <InputGroupAddon align="inline-end">
          <InputGroupText>{row.control.unit}</InputGroupText>
        </InputGroupAddon>
      ) : null}
    </InputGroup>
  );
}
```

### examples/src/app/components/ui/input.tsx

```ts
import * as React from 'react';

import { cn } from '@/lib/utils';

export const inputClassName =
  'flex h-11 w-full rounded-lg border border-input bg-card px-4 py-3 text-action font-normal transition-colors file:border-0 file:bg-transparent file:text-action file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-destructive disabled:cursor-not-allowed disabled:opacity-50';

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
  ({ className, type, ...props }, ref) => {
    return <input type={type} className={cn(inputClassName, className)} ref={ref} {...props} />;
  },
);
Input.displayName = 'Input';

export { Input };
```

### examples/src/playbooks/site-massing-download.tsx

```ts
import { useState } from 'react';
import { Button } from '../app/components/ui/button';
import { Field, FieldLabel } from '../app/components/ui/field';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../app/components/ui/select';
import { exportStudy } from './site-massing-exports';
import type { SiteMassingWorkspace } from './site-massing-workspace';

type StudyFormat = Parameters<typeof exportStudy>[1];
const formats: readonly { value: StudyFormat; label: string }[] = [
  { value: 'project', label: 'Editable project' },
  { value: 'options', label: 'Saved options' },
  { value: 'pdf', label: 'Site plan PDF' },
  { value: 'glb', label: '3D model GLB' },
  { value: 'csv', label: 'Report CSV' },
];

interface StudyDownloadProps {
  workspace: SiteMassingWorkspace;
  run: (action: () => unknown) => void;
}

export function StudyDownload({ workspace, run }: StudyDownloadProps) {
  const [format, setFormat] = useState<StudyFormat>('project');
  return (
    <Field>
      <FieldLabel htmlFor="study-download">Download</FieldLabel>
      <div className="flex gap-2">
        <Select
          value={format}
          onValueChange={(value) => {
            const chosen = formats.find((item) => item.value === value);
            if (chosen) setFormat(chosen.value);
          }}
        >
          <SelectTrigger id="study-download" className="min-w-0 flex-1">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {formats.map((item) => (
              <SelectItem key={item.value} value={item.value}>
                {item.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <Button
          variant="outline"
          disabled={workspace.busy}
          data-action={`export-${format}`}
          onClick={() => run(() => exportStudy(workspace, format))}
        >
          Download
        </Button>
      </div>
      <p className="text-xs text-muted-foreground">Keep the editable project to continue later.</p>
    </Field>
  );
}
```

### examples/src/app/lib/fit-scene.ts

```ts
import { Box3, InstancedMesh, Line, Mesh, PerspectiveCamera, Points, Scene, Vector3 } from 'three';
import type { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/** Fit visible model content while preserving the viewing direction. */
export function fitScene(scene: Scene, camera: PerspectiveCamera, controls: OrbitControls): void {
  const bounds = new Box3();
  scene.updateMatrixWorld(true);
  scene.traverseVisible((object) => {
    if (!(object instanceof Mesh || object instanceof Line || object instanceof Points)) return;
    if (object.type === 'GridHelper') return;
    // Fully transparent helpers (such as the glyph library's 100 m pick plane) are not content.
    const materials = [object.material].flat();
    if (materials.every((material) => material.transparent && material.opacity === 0)) return;
    if (object instanceof InstancedMesh) object.computeBoundingBox();
    else if (!object.geometry.boundingBox) object.geometry.computeBoundingBox();
    const local =
      object instanceof InstancedMesh ? object.boundingBox : object.geometry.boundingBox;
    if (!local) return;
    const box = local.clone().applyMatrix4(object.matrixWorld);
    if (!box.isEmpty() && [...box.min.toArray(), ...box.max.toArray()].every(Number.isFinite))
      bounds.union(box);
  });
  if (bounds.isEmpty()) return;
  const center = bounds.getCenter(new Vector3());
  const radius = bounds.getSize(new Vector3()).length() / 2;
  if (radius <= 0) return;
  const vertical = (camera.getEffectiveFOV() * Math.PI) / 180;
  const horizontal = 2 * Math.atan(Math.tan(vertical / 2) * camera.aspect);
  const distance = (radius / Math.sin(Math.min(vertical, horizontal) / 2)) * 1.08;
  const direction = camera.position.clone().sub(controls.target).normalize();
  if (direction.lengthSq() === 0) direction.set(1, 1, 1).normalize();
  camera.position.copy(center).addScaledVector(direction, distance);
  controls.target.copy(center);
  controls.update();
}
```

### examples/src/app/components/shell/inspector-number.ts

```ts
/** Parse a complete finite decimal draft; empty and partial entries stay uncommitted. */
export function parseNumberDraft(draft: string): number | undefined {
  const text = draft.trim();
  if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) return undefined;
  const value = Number(text);
  return Number.isFinite(value) ? value : undefined;
}

/** Read numeric axes without assuming that an SDK-provided unknown value is a vector. */
export function numericComponents(value: unknown, size: number): Array<number | undefined> {
  const values: unknown[] = size === 1 ? [value] : Array.isArray(value) ? value : [];
  return Array.from({ length: size }, (_, index) => {
    const component = values[index];
    return typeof component === 'number' && Number.isFinite(component) ? component : undefined;
  });
}
```

### examples/src/playbooks/site-massing-exports.ts

```ts
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import { download } from '../app/lib/download';
import { residentialEstimate, studyReport, type StudyAssumptions } from './code/site-massing';
import type { ZoningReport } from '@opengeometry/floorkit';
import type { SiteMassingWorkspace } from './site-massing-workspace';

const metricUnits = new Map<string, string>([
  ['siteArea', 'm²'],
  ['footprintArea', 'm²'],
  ['grossFloorArea', 'm²'],
  ['countedFloorArea', 'm²'],
  ['floorAreaRatio', 'ratio'],
  ['siteCoverage', 'ratio'],
  ['volumeRatio', 'm³/m²'],
  ['maxHeight', 'm'],
]);

export async function exportStudy(
  workspace: SiteMassingWorkspace,
  format: 'project' | 'options' | 'pdf' | 'glb' | 'csv',
): Promise<void> {
  const manager = workspace.manager;
  if (format === 'project')
    download('site-study.floorkit.json', manager.save(), 'application/json');
  if (format === 'options')
    download(
      'site-study-options.json',
      JSON.stringify(
        { basis: 'Illustrative controls; flat grade', options: workspace.options },
        null,
        2,
      ),
      'application/json',
    );
  if (format === 'pdf') download('site-plan.pdf', await manager.exportSheet('site-sheet', 'pdf'));
  if (format === 'csv') {
    const report = studyReport(manager);
    const basis = JSON.stringify(manager.session.snapshot().site);
    const rows = [
      ['section', 'option', 'quantity', 'value', 'unit', 'detail'],
      ...reportRows(
        'Current',
        report,
        workspace.assumptions,
        'Current site and rule basis',
        workspace.read().coverageComplete,
      ),
      ...workspace.options.flatMap((option) =>
        reportRows(
          option.name,
          option.report,
          option.assumptions,
          option.siteBasis === basis ? 'same basis' : 'different basis',
          option.coverageComplete,
        ),
      ),
    ];
    download(
      'site-study.csv',
      rows.map((row) => row.map(csvCell).join(',')).join('\r\n'),
      'text/csv',
    );
  }
  if (format === 'glb') {
    manager.setBuildableEnvelope(false);
    try {
      manager.root.updateMatrixWorld(true);
      const glb = await new GLTFExporter().parseAsync(manager.root, {
        binary: true,
        onlyVisible: true,
      });
      if (!(glb instanceof ArrayBuffer)) throw new Error('GLB export did not produce binary data.');
      download('site-study.glb', new Blob([glb], { type: 'model/gltf-binary' }));
    } finally {
      manager.setBuildableEnvelope(workspace.envelope);
    }
  }
}

function csvCell(value: string): string {
  return /[",\r\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
}

function reportRows(
  name: string,
  report: ZoningReport,
  assumptions: StudyAssumptions,
  basis: string,
  coverageComplete: boolean,
): string[][] {
  const estimate = residentialEstimate(report, assumptions);
  return [
    ['basis', name, 'controls', 'illustrative', '', basis],
    [
      'basis',
      name,
      'coverageComplete',
      String(coverageComplete),
      '',
      coverageComplete ? 'All masses start at grade' : 'Check coverage separately',
    ],
    ...Object.entries(report.metrics)
      .filter(([, value]) => typeof value === 'number')
      .map(([key, value]) => ['metrics', name, key, String(value), metricUnits.get(key) ?? '', '']),
    [
      'metrics',
      name,
      'grossVolume',
      String(report.metrics.perMass.reduce((sum, mass) => sum + mass.grossVolume, 0)),
      'm³',
      'Sum of all mass parts',
    ],
    [
      'assumptions',
      name,
      'residentialEfficiency',
      String(assumptions.residentialEfficiency),
      'ratio',
      'estimate',
    ],
    ['assumptions', name, 'averageUnitArea', String(assumptions.averageUnitArea), 'm²', 'estimate'],
    ...Object.entries(estimate).map(([key, value]) => [
      'estimate',
      name,
      key,
      String(value),
      key === 'estimatedUnits' ? 'units' : 'm²',
      'Arithmetic only; physical fit not checked',
    ]),
    ['zoning', name, 'compliant', String(report.compliant), '', 'Encoded checks only'],
    ...report.violations.map((v) => [
      'violation',
      name,
      v.type,
      String(v.current ?? ''),
      '',
      `${v.ruleId ?? ''}; ${v.massId ?? v.masses?.join(' + ') ?? ''}; edge ${v.edgeIndex ?? ''}; limit ${v.limit ?? ''}`,
    ]),
  ];
}
```

### examples/src/app/lib/download.ts

```ts
export function download(filename: string, content: Blob | string, type = 'application/octet-stream') {
  const blob = typeof content === 'string' ? new Blob([content], { type }) : content;
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();
  setTimeout(() => URL.revokeObjectURL(url), 0);
}
```
