{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "design-canvas-block",
  "type": "registry:block",
  "title": "Design canvas",
  "description": "Figma-style editor with layers, an infinite canvas, a floating tool bar, and scrubbable property inputs.",
  "dependencies": [
    "lucide-react@^1.47.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/button.json",
    "https://control-ui.dev/r/color-picker.json",
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/empty.json",
    "https://control-ui.dev/r/infinite-canvas.json",
    "https://control-ui.dev/r/input-group.json",
    "https://control-ui.dev/r/number-field.json",
    "https://control-ui.dev/r/resizable.json",
    "https://control-ui.dev/r/scroll-area.json",
    "https://control-ui.dev/r/separator.json",
    "https://control-ui.dev/r/toggle.json",
    "https://control-ui.dev/r/toolbar.json",
    "https://control-ui.dev/r/tooltip.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/control-ui/design-canvas-data.ts",
      "target": "@components/control-ui/blocks/design-canvas-data.ts",
      "type": "registry:block",
      "content": "export type DesignCanvasLayerKind = \"frame\" | \"rectangle\" | \"ellipse\" | \"text\";\n\nexport type DesignCanvasTool = \"move\" | DesignCanvasLayerKind;\n\nexport type DesignCanvasFill = { color: string; opacity: number; visible: boolean };\n\nexport type DesignCanvasLayer = {\n  id: string;\n  name: string;\n  kind: DesignCanvasLayerKind;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  rotation: number;\n  opacity: number;\n  cornerRadius: number;\n  fill: DesignCanvasFill | null;\n  text?: string;\n};\n\nexport type DesignCanvasPoint = { x: number; y: number };\n\nexport type DesignCanvasBounds = Pick<DesignCanvasLayer, \"x\" | \"y\" | \"width\" | \"height\">;\n\nconst NEW_LAYER_DEFAULTS: Record<DesignCanvasLayerKind, Pick<DesignCanvasLayer, \"name\" | \"width\" | \"height\" | \"fill\" | \"text\">> = {\n  frame: { name: \"Frame\", width: 320, height: 200, fill: { color: \"#FFFFFF\", opacity: 100, visible: true } },\n  rectangle: { name: \"Rectangle\", width: 120, height: 120, fill: { color: \"#D9D9D9\", opacity: 100, visible: true } },\n  ellipse: { name: \"Ellipse\", width: 120, height: 120, fill: { color: \"#D9D9D9\", opacity: 100, visible: true } },\n  text: { name: \"Text\", width: 160, height: 32, fill: { color: \"#000000\", opacity: 100, visible: true }, text: \"Text\" },\n};\n\nexport function drawnDesignCanvasBounds(\n  kind: DesignCanvasLayerKind,\n  start: DesignCanvasPoint,\n  end: DesignCanvasPoint,\n  clickTolerance: number,\n): DesignCanvasBounds {\n  const width = Math.abs(end.x - start.x);\n  const height = Math.abs(end.y - start.y);\n  if (Math.max(width, height) < clickTolerance) {\n    const defaults = NEW_LAYER_DEFAULTS[kind];\n    return {\n      x: Math.round(start.x - defaults.width / 2),\n      y: Math.round(start.y - defaults.height / 2),\n      width: defaults.width,\n      height: defaults.height,\n    };\n  }\n  return {\n    x: Math.round(Math.min(start.x, end.x)),\n    y: Math.round(Math.min(start.y, end.y)),\n    width: Math.max(1, Math.round(width)),\n    height: Math.max(1, Math.round(height)),\n  };\n}\n\nexport function createDesignCanvasLayer(kind: DesignCanvasLayerKind, bounds: DesignCanvasBounds, id: string): DesignCanvasLayer {\n  return { ...NEW_LAYER_DEFAULTS[kind], ...bounds, id, kind, rotation: 0, opacity: 100, cornerRadius: 0 };\n}\n\nexport function resizeDesignCanvasLayer(\n  layer: DesignCanvasLayer,\n  size: Partial<Pick<DesignCanvasLayer, \"width\" | \"height\">>,\n  aspectRatioLocked: boolean,\n): DesignCanvasLayer {\n  const aspectRatio = layer.width / layer.height;\n  if (size.width !== undefined) {\n    const height = aspectRatioLocked ? Math.round(size.width / aspectRatio) : layer.height;\n    return { ...layer, width: size.width, height };\n  }\n  if (size.height !== undefined) {\n    const width = aspectRatioLocked ? Math.round(size.height * aspectRatio) : layer.width;\n    return { ...layer, width, height: size.height };\n  }\n  return layer;\n}\n\nexport function designCanvasFillColor(fill: DesignCanvasFill | null) {\n  if (!fill?.visible) return \"transparent\";\n  return `color-mix(in srgb, ${fill.color} ${fill.opacity}%, transparent)`;\n}\n"
    },
    {
      "path": "src/registry/blocks/control-ui/design-canvas-inspector.tsx",
      "target": "@components/control-ui/blocks/design-canvas-inspector.tsx",
      "type": "registry:block",
      "content": "\"use client\";\n\nimport {\n  BlendIcon,\n  EyeIcon,\n  EyeOffIcon,\n  Link2Icon,\n  MinusIcon,\n  PlusIcon,\n  RotateCwIcon,\n  SquareRoundCornerIcon,\n  Unlink2Icon,\n} from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { useState } from \"react\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport {\n  ColorPicker,\n  ColorPickerArea,\n  ColorPickerContent,\n  ColorPickerEyeDropper,\n  ColorPickerHue,\n  ColorPickerInput,\n  ColorPickerTrigger,\n} from \"@/components/control-ui/ui/color-picker\";\nimport { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from \"@/components/control-ui/ui/empty\";\nimport { InputGroup, InputGroupAddon } from \"@/components/control-ui/ui/input-group\";\nimport { NumberField, NumberFieldGroup, NumberFieldInput, NumberFieldScrubArea } from \"@/components/control-ui/ui/number-field\";\nimport { Separator } from \"@/components/control-ui/ui/separator\";\nimport { Toggle } from \"@/components/control-ui/ui/toggle\";\nimport { type DesignCanvasFill, type DesignCanvasLayer, resizeDesignCanvasLayer } from \"./design-canvas-data\";\n\nconst WHOLE_NUMBER_FORMAT: Intl.NumberFormatOptions = { useGrouping: false, maximumFractionDigits: 0 };\nconst DEFAULT_FILL: DesignCanvasFill = { color: \"#D9D9D9\", opacity: 100, visible: true };\n\nexport type DesignCanvasInspectorProps = {\n  layer: DesignCanvasLayer | undefined;\n  onLayerChange: (layer: DesignCanvasLayer) => void;\n};\n\nexport function DesignCanvasInspector({ layer, onLayerChange }: DesignCanvasInspectorProps) {\n  const [aspectRatioLocked, setAspectRatioLocked] = useState(false);\n\n  if (!layer) {\n    return (\n      <Empty className=\"h-full\">\n        <EmptyHeader>\n          <EmptyTitle>Nothing selected</EmptyTitle>\n          <EmptyDescription>Select a layer to edit its position, size, and fill.</EmptyDescription>\n        </EmptyHeader>\n      </Empty>\n    );\n  }\n\n  const update = (patch: Partial<DesignCanvasLayer>) => onLayerChange({ ...layer, ...patch });\n\n  return (\n    <div className=\"flex flex-col\">\n      <InspectorSection title=\"Position\">\n        <ScrubField label=\"X position\" prefix=\"X\" value={layer.x} onValueChange={(x) => update({ x })} />\n        <ScrubField label=\"Y position\" prefix=\"Y\" value={layer.y} onValueChange={(y) => update({ y })} />\n        <ScrubField\n          label=\"Rotation\"\n          className=\"col-start-1\"\n          prefix={<RotateCwIcon />}\n          value={layer.rotation}\n          min={-360}\n          max={360}\n          format={{ ...WHOLE_NUMBER_FORMAT, style: \"unit\", unit: \"degree\", unitDisplay: \"narrow\" }}\n          onValueChange={(rotation) => update({ rotation })}\n        />\n      </InspectorSection>\n\n      <InspectorSection title=\"Layout\">\n        <ScrubField\n          label=\"Width\"\n          prefix=\"W\"\n          value={layer.width}\n          min={1}\n          onValueChange={(width) => onLayerChange(resizeDesignCanvasLayer(layer, { width }, aspectRatioLocked))}\n        />\n        <ScrubField\n          label=\"Height\"\n          prefix=\"H\"\n          value={layer.height}\n          min={1}\n          onValueChange={(height) => onLayerChange(resizeDesignCanvasLayer(layer, { height }, aspectRatioLocked))}\n        />\n        <Toggle\n          size=\"sm\"\n          variant=\"ghost\"\n          iconOnly\n          aria-label=\"Lock aspect ratio\"\n          pressed={aspectRatioLocked}\n          onPressedChange={setAspectRatioLocked}\n        >\n          {aspectRatioLocked ? <Link2Icon /> : <Unlink2Icon />}\n        </Toggle>\n      </InspectorSection>\n\n      <InspectorSection title=\"Appearance\">\n        <ScrubField\n          label=\"Layer opacity\"\n          prefix={<BlendIcon />}\n          suffix=\"%\"\n          value={layer.opacity}\n          min={0}\n          max={100}\n          onValueChange={(opacity) => update({ opacity })}\n        />\n        <ScrubField\n          label=\"Corner radius\"\n          prefix={<SquareRoundCornerIcon />}\n          value={layer.cornerRadius}\n          min={0}\n          onValueChange={(cornerRadius) => update({ cornerRadius })}\n        />\n      </InspectorSection>\n\n      <InspectorSection\n        title=\"Fill\"\n        action={\n          layer.fill ? null : (\n            <Button size=\"xs\" variant=\"ghost\" iconOnly aria-label=\"Add fill\" onClick={() => update({ fill: DEFAULT_FILL })}>\n              <PlusIcon />\n            </Button>\n          )\n        }\n      >\n        {layer.fill ? (\n          <FillRow fill={layer.fill} onFillChange={(fill) => update({ fill })} onRemove={() => update({ fill: null })} />\n        ) : null}\n      </InspectorSection>\n    </div>\n  );\n}\n\nfunction InspectorSection({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {\n  return (\n    <section aria-label={title} className=\"flex flex-col gap-2 border-b px-3 py-3\">\n      <header className=\"flex h-6 items-center justify-between\">\n        <h3 className=\"text-caption font-medium\">{title}</h3>\n        {action}\n      </header>\n      <div className=\"grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-2 empty:hidden\">{children}</div>\n    </section>\n  );\n}\n\ntype ScrubFieldProps = {\n  label: string;\n  className?: string;\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n  value: number;\n  onValueChange: (value: number) => void;\n  min?: number;\n  max?: number;\n  format?: Intl.NumberFormatOptions;\n};\n\nfunction ScrubField({ label, className, prefix, suffix, value, onValueChange, min, max, format = WHOLE_NUMBER_FORMAT }: ScrubFieldProps) {\n  return (\n    <NumberField\n      size=\"sm\"\n      className={className}\n      value={value}\n      min={min}\n      max={max}\n      format={format}\n      onValueChange={(next) => {\n        if (next !== null) onValueChange(next);\n      }}\n    >\n      <NumberFieldGroup className=\"w-full\">\n        {prefix ? <NumberFieldScrubArea>{prefix}</NumberFieldScrubArea> : null}\n        <NumberFieldInput aria-label={label} />\n        {suffix ? <NumberFieldScrubArea>{suffix}</NumberFieldScrubArea> : null}\n      </NumberFieldGroup>\n    </NumberField>\n  );\n}\n\ntype FillRowProps = {\n  fill: DesignCanvasFill;\n  onFillChange: (fill: DesignCanvasFill) => void;\n  onRemove: () => void;\n};\n\nfunction FillRow({ fill, onFillChange, onRemove }: FillRowProps) {\n  return (\n    <>\n      <ColorPicker value={fill.color} alpha={false} onValueChange={(color) => onFillChange({ ...fill, color })}>\n        <InputGroup size=\"sm\" className=\"col-span-2\">\n          <InputGroupAddon>\n            <ColorPickerTrigger aria-label=\"Choose fill color\" />\n          </InputGroupAddon>\n          <ColorPickerInput aria-label=\"Fill color\" />\n          <Separator orientation=\"vertical\" className=\"h-4\" />\n          <NumberField\n            value={fill.opacity}\n            min={0}\n            max={100}\n            format={WHOLE_NUMBER_FORMAT}\n            className=\"shrink-0\"\n            onValueChange={(opacity) => {\n              if (opacity !== null) onFillChange({ ...fill, opacity });\n            }}\n          >\n            <NumberFieldGroup className=\"w-full\">\n              <NumberFieldInput aria-label=\"Fill opacity\" />\n              <NumberFieldScrubArea>%</NumberFieldScrubArea>\n            </NumberFieldGroup>\n          </NumberField>\n        </InputGroup>\n        <ColorPickerContent side=\"left\" align=\"start\">\n          <ColorPickerArea />\n          <div className=\"flex items-center gap-2\">\n            <ColorPickerEyeDropper />\n            <ColorPickerHue className=\"flex-1\" />\n          </div>\n        </ColorPickerContent>\n      </ColorPicker>\n      <div className=\"flex\">\n        <Toggle\n          size=\"sm\"\n          variant=\"ghost\"\n          iconOnly\n          aria-label=\"Show fill\"\n          pressed={fill.visible}\n          onPressedChange={(visible) => onFillChange({ ...fill, visible })}\n        >\n          {fill.visible ? <EyeIcon /> : <EyeOffIcon />}\n        </Toggle>\n        <Button size=\"sm\" variant=\"ghost\" iconOnly aria-label=\"Remove fill\" onClick={onRemove}>\n          <MinusIcon />\n        </Button>\n      </div>\n    </>\n  );\n}\n"
    },
    {
      "path": "src/registry/blocks/control-ui/design-canvas.tsx",
      "target": "@components/control-ui/blocks/design-canvas.tsx",
      "type": "registry:block",
      "content": "\"use client\";\n\nimport { CircleIcon, FrameIcon, MousePointer2Icon, SquareIcon, TypeIcon } from \"lucide-react\";\nimport type { ComponentProps, KeyboardEvent, PointerEvent, ReactNode } from \"react\";\nimport { useRef, useState } from \"react\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport {\n  InfiniteCanvas,\n  InfiniteCanvasContent,\n  InfiniteCanvasControls,\n  InfiniteCanvasItem,\n  type InfiniteCanvasTransform,\n} from \"@/components/control-ui/ui/infinite-canvas\";\nimport { ResizableFloatingPanel } from \"@/components/control-ui/ui/resizable\";\nimport { ScrollArea } from \"@/components/control-ui/ui/scroll-area\";\nimport { Toolbar, ToolbarButton, ToolbarGroup } from \"@/components/control-ui/ui/toolbar\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/control-ui/ui/tooltip\";\nimport {\n  createDesignCanvasLayer,\n  type DesignCanvasLayer,\n  type DesignCanvasLayerKind,\n  type DesignCanvasPoint,\n  type DesignCanvasTool,\n  designCanvasFillColor,\n  drawnDesignCanvasBounds,\n} from \"./design-canvas-data\";\nimport { DesignCanvasInspector } from \"./design-canvas-inspector\";\n\nexport type { DesignCanvasFill, DesignCanvasLayer, DesignCanvasLayerKind, DesignCanvasTool } from \"./design-canvas-data\";\n\nexport type DesignCanvasBlockProps = Omit<ComponentProps<\"div\">, \"children\" | \"defaultValue\" | \"onChange\"> & {\n  layers?: readonly DesignCanvasLayer[];\n  defaultLayers?: readonly DesignCanvasLayer[];\n  onLayersChange?: (layers: DesignCanvasLayer[]) => void;\n  selectedLayerId?: string | null;\n  defaultSelectedLayerId?: string | null;\n  onSelectedLayerIdChange?: (layerId: string | null) => void;\n  defaultTransform?: InfiniteCanvasTransform;\n  layout?: \"viewport\" | \"contained\";\n};\n\nconst TOOLS: readonly { tool: DesignCanvasTool; label: string; shortcut: string; icon: ReactNode }[] = [\n  { tool: \"move\", label: \"Move\", shortcut: \"V\", icon: <MousePointer2Icon /> },\n  { tool: \"frame\", label: \"Frame\", shortcut: \"F\", icon: <FrameIcon /> },\n  { tool: \"rectangle\", label: \"Rectangle\", shortcut: \"R\", icon: <SquareIcon /> },\n  { tool: \"ellipse\", label: \"Ellipse\", shortcut: \"O\", icon: <CircleIcon /> },\n  { tool: \"text\", label: \"Text\", shortcut: \"T\", icon: <TypeIcon /> },\n];\n\nconst LAYER_ICONS: Record<DesignCanvasLayerKind, ReactNode> = {\n  frame: <FrameIcon />,\n  rectangle: <SquareIcon />,\n  ellipse: <CircleIcon />,\n  text: <TypeIcon />,\n};\n\nconst NUDGE_DIRECTIONS: Partial<Record<string, { x: number; y: number }>> = {\n  ArrowLeft: { x: -1, y: 0 },\n  ArrowRight: { x: 1, y: 0 },\n  ArrowUp: { x: 0, y: -1 },\n  ArrowDown: { x: 0, y: 1 },\n};\n\nconst DEFAULT_TRANSFORM: InfiniteCanvasTransform = { x: 0, y: 0, scale: 1 };\nconst CLICK_TOLERANCE_PX = 4;\n\ntype DesignCanvasDraft = {\n  kind: DesignCanvasLayerKind;\n  pointerId: number;\n  start: DesignCanvasPoint;\n  end: DesignCanvasPoint;\n  clickTolerance: number;\n};\n\nfunction newLayerId(kind: DesignCanvasLayerKind) {\n  return `${kind}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction toolForShortcut(key: string) {\n  return TOOLS.find((entry) => entry.shortcut === key.toUpperCase())?.tool;\n}\n\nfunction layerFromDraft(draft: DesignCanvasDraft, id: string) {\n  return createDesignCanvasLayer(draft.kind, drawnDesignCanvasBounds(draft.kind, draft.start, draft.end, draft.clickTolerance), id);\n}\n\nexport function DesignCanvasBlock({\n  layers,\n  defaultLayers = [],\n  onLayersChange,\n  selectedLayerId,\n  defaultSelectedLayerId = null,\n  onSelectedLayerIdChange,\n  defaultTransform = DEFAULT_TRANSFORM,\n  layout = \"viewport\",\n  className,\n  ...props\n}: DesignCanvasBlockProps) {\n  const [internalLayers, setInternalLayers] = useState<readonly DesignCanvasLayer[]>(defaultLayers);\n  const [internalSelectedLayerId, setInternalSelectedLayerId] = useState(defaultSelectedLayerId);\n  const transformRef = useRef(defaultTransform);\n  const [tool, setTool] = useState<DesignCanvasTool>(\"move\");\n  const [draft, setDraft] = useState<DesignCanvasDraft | null>(null);\n  const currentLayers = layers ?? internalLayers;\n  const currentSelectedLayerId = selectedLayerId === undefined ? internalSelectedLayerId : selectedLayerId;\n  const selectedLayer = currentLayers.find((layer) => layer.id === currentSelectedLayerId);\n\n  function changeLayers(next: DesignCanvasLayer[]) {\n    if (layers === undefined) setInternalLayers(next);\n    onLayersChange?.(next);\n  }\n\n  function selectLayer(layerId: string | null) {\n    if (selectedLayerId === undefined) setInternalSelectedLayerId(layerId);\n    onSelectedLayerIdChange?.(layerId);\n  }\n\n  function replaceLayer(next: DesignCanvasLayer) {\n    changeLayers(currentLayers.map((layer) => (layer.id === next.id ? next : layer)));\n  }\n\n  function worldPointAt(event: PointerEvent<HTMLElement>) {\n    const bounds = event.currentTarget.getBoundingClientRect();\n    const { x, y, scale } = transformRef.current;\n    return { x: (event.clientX - bounds.left - x) / scale, y: (event.clientY - bounds.top - y) / scale };\n  }\n\n  function beginDrawOrDeselect(event: PointerEvent<HTMLElement>) {\n    if (event.defaultPrevented || event.target !== event.currentTarget) return;\n    if (tool === \"move\") {\n      selectLayer(null);\n      return;\n    }\n    if (!event.isPrimary || event.button !== 0) return;\n    event.preventDefault();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const point = worldPointAt(event);\n    setDraft({\n      kind: tool,\n      pointerId: event.pointerId,\n      start: point,\n      end: point,\n      clickTolerance: CLICK_TOLERANCE_PX / transformRef.current.scale,\n    });\n  }\n\n  function resizeDraft(event: PointerEvent<HTMLElement>) {\n    if (draft?.pointerId !== event.pointerId) return;\n    setDraft({ ...draft, end: worldPointAt(event) });\n  }\n\n  function commitDraft(event: PointerEvent<HTMLElement>) {\n    if (draft?.pointerId !== event.pointerId) return;\n    const layer = layerFromDraft(draft, newLayerId(draft.kind));\n    setDraft(null);\n    changeLayers([...currentLayers, layer]);\n    selectLayer(layer.id);\n    setTool(\"move\");\n  }\n\n  function cancelDraft(event: PointerEvent<HTMLElement>) {\n    if (draft?.pointerId === event.pointerId) setDraft(null);\n  }\n\n  function editSelectedLayerWithKey(event: KeyboardEvent<HTMLElement>) {\n    if (!selectedLayer) return false;\n    const nudge = NUDGE_DIRECTIONS[event.key];\n    if (event.key === \"Delete\" || event.key === \"Backspace\") {\n      changeLayers(currentLayers.filter((layer) => layer.id !== selectedLayer.id));\n      selectLayer(null);\n    } else if (nudge) {\n      const distance = event.shiftKey ? 10 : 1;\n      replaceLayer({ ...selectedLayer, x: selectedLayer.x + nudge.x * distance, y: selectedLayer.y + nudge.y * distance });\n    } else return false;\n    return true;\n  }\n\n  function handleShortcut(event: KeyboardEvent<HTMLElement>) {\n    if (event.metaKey || event.ctrlKey || event.altKey) return;\n    const shortcutTool = toolForShortcut(event.key);\n    if (shortcutTool) setTool(shortcutTool);\n    else if (event.key === \"Escape\") {\n      setDraft(null);\n      setTool(\"move\");\n      selectLayer(null);\n    } else if (!editSelectedLayerWithKey(event)) return;\n    event.preventDefault();\n  }\n\n  return (\n    <div\n      className={cn(\n        \"@container/design-canvas relative isolate overflow-hidden bg-background text-foreground\",\n        layout === \"contained\" ? \"h-full min-h-0\" : \"h-svh\",\n        className,\n      )}\n      {...props}\n    >\n      <InfiniteCanvas\n        aria-label=\"Design canvas\"\n        defaultTransform={defaultTransform}\n        onTransformChange={(next) => {\n          transformRef.current = next;\n        }}\n        data-tool={tool}\n        className={cn(\"size-full\", tool !== \"move\" && \"cursor-crosshair\")}\n        style={{\n          \"--cui-infinite-canvas-radius\": \"0px\",\n          \"--cui-infinite-canvas-border-color\": \"transparent\",\n          \"--cui-infinite-canvas-background\": \"var(--muted)\",\n        }}\n        onPointerDown={beginDrawOrDeselect}\n        onPointerMove={resizeDraft}\n        onPointerUp={commitDraft}\n        onPointerCancel={cancelDraft}\n        onKeyDown={handleShortcut}\n      >\n        <InfiniteCanvasContent className={cn(tool !== \"move\" && \"[&>*]:pointer-events-none\")}>\n          {currentLayers.map((layer) => (\n            <DesignCanvasLayerNode\n              key={layer.id}\n              layer={layer}\n              selected={layer.id === currentSelectedLayerId}\n              onSelect={() => selectLayer(layer.id)}\n              onLayerChange={replaceLayer}\n            />\n          ))}\n          {draft ? <DesignCanvasLayerNode layer={layerFromDraft(draft, \"draft\")} selected /> : null}\n        </InfiniteCanvasContent>\n        <InfiniteCanvasControls className=\"top-3 right-auto bottom-auto left-3 @4xl/design-canvas:left-1/2 @4xl/design-canvas:-translate-x-1/2\" />\n      </InfiniteCanvas>\n\n      <ResizableFloatingPanel side=\"left\" defaultSize={224} minSize={180} maxSize={360} className=\"hidden @4xl/design-canvas:flex\">\n        <aside aria-label=\"Layers\" className=\"flex min-h-0 flex-1 flex-col\">\n          <h2 className=\"flex h-11 shrink-0 items-center border-b px-3 text-caption font-medium\">Layers</h2>\n          <ScrollArea className=\"min-h-0 flex-1\">\n            <ul className=\"flex flex-col gap-0.5 p-2\">\n              {currentLayers.toReversed().map((layer) => (\n                <li key={layer.id}>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    active={layer.id === currentSelectedLayerId}\n                    aria-pressed={layer.id === currentSelectedLayerId}\n                    className=\"w-full justify-start [&_svg]:size-3.5 [&_svg]:text-muted-foreground\"\n                    onClick={() => selectLayer(layer.id)}\n                  >\n                    {LAYER_ICONS[layer.kind]}\n                    <span className=\"truncate\">{layer.name}</span>\n                  </Button>\n                </li>\n              ))}\n            </ul>\n          </ScrollArea>\n        </aside>\n      </ResizableFloatingPanel>\n\n      <ResizableFloatingPanel defaultSize={280} minSize={256} maxSize={440} className=\"hidden @xl/design-canvas:flex\">\n        <aside aria-label=\"Properties\" className=\"flex min-h-0 flex-1 flex-col\">\n          <h2 className=\"flex h-11 shrink-0 items-center border-b px-3 text-caption font-medium\">\n            <span className=\"truncate\">{selectedLayer?.name ?? \"Design\"}</span>\n          </h2>\n          <ScrollArea className=\"min-h-0 flex-1\">\n            <DesignCanvasInspector layer={selectedLayer} onLayerChange={replaceLayer} />\n          </ScrollArea>\n        </aside>\n      </ResizableFloatingPanel>\n\n      <DesignCanvasToolbar tool={tool} onToolChange={setTool} />\n    </div>\n  );\n}\n\ntype DesignCanvasLayerNodeProps = {\n  layer: DesignCanvasLayer;\n  selected: boolean;\n  onSelect?: () => void;\n  onLayerChange?: (layer: DesignCanvasLayer) => void;\n};\n\nfunction DesignCanvasLayerNode({ layer, selected, onSelect, onLayerChange }: DesignCanvasLayerNodeProps) {\n  const fillColor = designCanvasFillColor(layer.fill);\n  return (\n    <InfiniteCanvasItem\n      x={layer.x}\n      y={layer.y}\n      aria-label={layer.name}\n      data-selected={selected || undefined}\n      className=\"outline-primary data-selected:outline-2\"\n      style={{ width: layer.width, height: layer.height, rotate: `${layer.rotation}deg`, opacity: layer.opacity / 100 }}\n      onPointerDown={onSelect}\n      onPositionChange={onLayerChange && ((position) => onLayerChange({ ...layer, x: Math.round(position.x), y: Math.round(position.y) }))}\n    >\n      {layer.kind === \"frame\" ? (\n        <span className=\"absolute bottom-full left-0 mb-1 whitespace-nowrap text-caption text-muted-foreground\">{layer.name}</span>\n      ) : null}\n      {layer.kind === \"text\" ? (\n        <p className=\"size-full overflow-hidden text-2xl leading-8 whitespace-nowrap\" style={{ color: fillColor }}>\n          {layer.text}\n        </p>\n      ) : (\n        <div className=\"size-full\" style={{ background: fillColor, borderRadius: layer.kind === \"ellipse\" ? \"50%\" : layer.cornerRadius }} />\n      )}\n      {selected ? (\n        <span className=\"absolute top-full left-1/2 mt-1.5 -translate-x-1/2 rounded-sm bg-primary px-1 text-[10px] leading-4 whitespace-nowrap text-primary-foreground tabular-nums\">\n          {layer.width} × {layer.height}\n        </span>\n      ) : null}\n    </InfiniteCanvasItem>\n  );\n}\n\nfunction DesignCanvasToolbar({ tool, onToolChange }: { tool: DesignCanvasTool; onToolChange: (tool: DesignCanvasTool) => void }) {\n  return (\n    <TooltipProvider>\n      <Toolbar\n        variant=\"floating\"\n        aria-label=\"Design tools\"\n        className=\"absolute bottom-4 left-4 w-max @4xl/design-canvas:left-1/2 @4xl/design-canvas:-translate-x-1/2\"\n      >\n        <ToolbarGroup aria-label=\"Tools\">\n          {TOOLS.map((entry) => (\n            <Tooltip key={entry.tool}>\n              <TooltipTrigger\n                render={\n                  <ToolbarButton\n                    aria-label={entry.label}\n                    iconOnly\n                    aria-pressed={tool === entry.tool}\n                    data-pressed={tool === entry.tool ? \"\" : undefined}\n                    onClick={() => onToolChange(entry.tool)}\n                  />\n                }\n              >\n                {entry.icon}\n              </TooltipTrigger>\n              <TooltipContent>\n                {entry.label} · {entry.shortcut}\n              </TooltipContent>\n            </Tooltip>\n          ))}\n        </ToolbarGroup>\n      </Toolbar>\n    </TooltipProvider>\n  );\n}\n"
    }
  ]
}
