{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-chrome",
  "title": "Tool chrome",
  "description": "Shared editing plumbing for box-shaped mods: EditingBoxShapeTool, useShapeEditing, ToolChrome slots, and self-sizing ToolChromeField.",
  "dependencies": [
    "cn"
  ],
  "files": [
    {
      "path": "workspace/src/tool-chrome.tsx",
      "content": "import { useEffect, useRef, type ChangeEvent, type ComponentPropsWithoutRef, type KeyboardEvent, type ReactNode, type SyntheticEvent } from 'react'\nimport { BaseBoxShapeTool, useValue, type Editor, type StateNode, type TLShape, type TLShapeId, type TLStateNodeConstructor } from 'tldraw'\nimport { cn } from '@/lib/utils'\n\nconst stop = (event: SyntheticEvent) => event.stopPropagation()\nconst sides = ['top', 'right', 'bottom', 'left'] as const\ntype Side = (typeof sides)[number]\n\n// Each edge sits 8 canvas units outside the frame; top/bottom lay controls out in a row, left/right in a column.\nconst edgeClass: Record<Side, string> = {\n\ttop: 'left-0 w-full flex-row bottom-[calc(100%+8px)]',\n\tbottom: 'left-0 w-full flex-row top-[calc(100%+8px)]',\n\tleft: 'top-0 h-full flex-col right-[calc(100%+8px)]',\n\tright: 'top-0 h-full flex-col left-[calc(100%+8px)]',\n}\n\nexport interface ShapeEditing {\n\tediting: boolean\n\treadonly: boolean\n\tdark: boolean\n\t/** Leave editing: back to the select tool with the canvas focused. */\n\tfinish(): void\n}\n\n// Editing state every custom shape reads, plus the one way to leave editing.\nexport function useShapeEditing(editor: Editor, shape: { id: TLShapeId }): ShapeEditing {\n\tconst editing = useValue('shape editing', () => editor.getEditingShapeId() === shape.id, [editor, shape.id])\n\tconst readonly = useValue('shape readonly', () => editor.getInstanceState().isReadonly || editor.isShapeOrAncestorLocked(shape.id), [editor, shape.id])\n\tconst dark = useValue('shape theme', () => editor.user.getUserPreferences().isDarkMode, [editor])\n\tfunction finish() {\n\t\teditor.setEditingShape(null)\n\t\teditor.setCurrentTool('select')\n\t\teditor.focus()\n\t}\n\treturn { editing, readonly, dark, finish }\n}\n\ntype TextControl = HTMLInputElement | HTMLTextAreaElement\n\nexport interface ToolChromeFieldProps extends Omit<ComponentPropsWithoutRef<'input'>, 'onChange' | 'value' | 'onKeyDown'> {\n\tvalue: string\n\t/** The control's own classes (padding, line-height); the span's `className` sizes the field. */\n\tcontrolClassName?: string\n\tonChange?: (event: ChangeEvent<TextControl>) => void\n\t/** A textarea that grows with its content instead of a single-line input. */\n\tmultiline?: boolean\n\t/** Take focus while true. */\n\tfocus?: boolean\n\t/** Also select the text on focus, for renames. */\n\tselectOnFocus?: boolean\n\t/** 'enter' submits the form on Enter (Shift+Enter breaks a line when multiline); 'mod-enter' submits on ⌘/Ctrl+Enter and lets Enter break lines. */\n\tsubmit?: 'enter' | 'mod-enter'\n\tonEscape?: () => void\n}\n\n// The one text field for shape chrome. Controlled; sizes to its content; keeps keys away from the canvas.\nexport function ToolChromeField({ multiline = false, focus = false, selectOnFocus = false, submit = 'enter', onEscape, className, controlClassName, value, placeholder, ...props }: ToolChromeFieldProps) {\n\tconst control = useRef<TextControl>(null)\n\tuseEffect(() => {\n\t\tif (!focus) return\n\t\tcontrol.current?.focus()\n\t\tif (selectOnFocus) control.current?.select()\n\t}, [focus, selectOnFocus])\n\tfunction onKeyDown(event: KeyboardEvent<TextControl>) {\n\t\tevent.stopPropagation()\n\t\tif (event.nativeEvent.isComposing) return\n\t\tif (event.key === 'Escape') { event.preventDefault(); onEscape?.() }\n\t\telse if (event.key === 'Enter') {\n\t\t\tconst submits = submit === 'mod-enter' ? event.metaKey || event.ctrlKey : !(multiline && event.shiftKey)\n\t\t\tif (submits) { event.preventDefault(); event.currentTarget.form?.requestSubmit() }\n\t\t}\n\t}\n\t// A hidden mirror of the text (::after) sizes the grid cell; the control fills it. Single-line fields grow in width, multiline in height.\n\tconst shared = {\n\t\tvalue, placeholder, autoComplete: 'off', spellCheck: false, ...props, onKeyDown,\n\t\tclassName: cn('[grid-area:1/1] box-border h-full min-h-8 w-full min-w-0 resize-none overflow-auto border-0 bg-transparent px-1 py-[7px] text-inherit text-ellipsis outline-0 select-text [font:inherit] leading-[18px] placeholder:text-muted-foreground', controlClassName),\n\t}\n\treturn (\n\t\t<span\n\t\t\tclassName={cn('relative inline-grid min-w-0 max-w-full items-center after:invisible after:[grid-area:1/1] after:px-1 after:py-[7px] after:[font:inherit] after:leading-[18px] after:whitespace-pre after:content-[attr(data-value)_\"_\"]', multiline && 'max-h-26 after:whitespace-pre-wrap after:[overflow-wrap:anywhere]', className)}\n\t\t\tdata-value={value || placeholder || ''}\n\t\t>\n\t\t\t{multiline\n\t\t\t\t? <textarea ref={control as React.RefObject<HTMLTextAreaElement>} rows={1} {...(shared as ComponentPropsWithoutRef<'textarea'>)} />\n\t\t\t\t: <input ref={control as React.RefObject<HTMLInputElement>} {...(shared as ComponentPropsWithoutRef<'input'>)} />}\n\t\t</span>\n\t)\n}\n\n// Slots follow the frame's bounds without changing its geometry or exported content.\nexport function ToolChrome(slots: Partial<Record<Side, ReactNode>>) {\n\treturn (\n\t\t<>\n\t\t\t{sides.map(side => slots[side] ? (\n\t\t\t\t<div key={side} className={cn('pointer-events-none absolute flex items-center justify-between gap-3', edgeClass[side])} onPointerDown={stop} onPointerUp={stop} onDoubleClick={stop} onKeyDown={stop} onKeyUp={stop}>\n\t\t\t\t\t{slots[side]}\n\t\t\t\t</div>\n\t\t\t) : null)}\n\t\t</>\n\t)\n}\n\nexport interface ToolChromeGroupProps extends ComponentPropsWithoutRef<'form'> {\n\tas?: 'div' | 'form'\n\t/** 'panel' for a raised control strip, 'well' for a group that holds a text field. */\n\tsurface?: 'panel' | 'well'\n}\n\n// Shape chrome is always the pill variant. Both elements accept the same attribute set; the form's types are the superset.\nexport function ToolChromeGroup({ as = 'div', surface = 'panel', className = '', children, ...props }: ToolChromeGroupProps) {\n\tconst Tag = as as 'div'\n\treturn <Tag {...(props as ComponentPropsWithoutRef<'div'>)} className={cn(`ui-${surface} ui-round pointer-events-auto flex min-h-10 [flex-direction:inherit] items-center gap-0.5 p-1`, className)}>{children}</Tag>\n}\n\n// A box tool whose new shape opens for editing at once, so its input can take focus and the user keeps typing.\n// BaseBoxShapeTool only reports drag-created shapes through onCreate; click placement finishes inside Pointing,\n// whose `complete()` the SDK does not declare.\ninterface PointingConstructor extends Omit<TLStateNodeConstructor, 'children'> {\n\tnew (editor: Editor, parent?: StateNode): StateNode & { complete(): void }\n}\nconst [BoxIdle, BoxPointing] = BaseBoxShapeTool.children() as unknown as [TLStateNodeConstructor, PointingConstructor]\nclass EditingPointing extends BoxPointing {\n\toverride complete() {\n\t\tsuper.complete()\n\t\tconst shape = this.editor.getOnlySelectedShape()\n\t\tif (shape?.type === (this.parent as BaseBoxShapeTool).shapeType) this.editor.setEditingShape(shape.id)\n\t}\n}\n\nexport abstract class EditingBoxShapeTool extends BaseBoxShapeTool {\n\tstatic override children(): TLStateNodeConstructor[] { return [BoxIdle, EditingPointing] }\n\t// The editor's page-state side effect moves the select tool into editing.\n\toverride onCreate(shape: TLShape | null) { if (shape) this.editor.setEditingShape(shape.id) }\n}\n",
      "type": "registry:lib",
      "target": "~/src/tool-chrome.tsx"
    },
    {
      "path": "workspace/src/lib/utils.ts",
      "content": "export { cn } from 'cn'\n",
      "type": "registry:lib",
      "target": "~/src/lib/utils.ts"
    }
  ],
  "categories": [
    "lib"
  ],
  "type": "registry:lib"
}