{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "landmark",
  "title": "Landmark",
  "description": "A hollow dashed named area with a constant-screen-size pin chip. Clicks inside fall through to the shapes it marks; a brush must enclose it fully. Adds a Create landmark command that wraps the selection.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "btn0s/tldraw-mods/tool-chrome"
  ],
  "files": [
    {
      "path": "workspace/src/mods/landmark.tsx",
      "content": "import { MapPin } from 'lucide-react'\nimport type { ModCommand, ModConfig, ModTool } from '@/mod'\nimport { Group2d, HTMLContainer, Rectangle2d, ShapeUtil, T, createShapeId, resizeBox, useValue, type Editor, type RecordProps, type TLBaseShape, type TLResizeInfo } from 'tldraw'\nimport { cn } from '@/lib/utils'\nimport { EditingBoxShapeTool, ToolChromeField, useShapeEditing } from '@/tool-chrome'\n\n// The chip keeps a constant screen size, like a frame heading, so a landmark stays readable when zoomed out.\nconst chipHeight = 28\nconst chipGap = 6\nconst chipPadding = 8 + 14 + 6 + 10 // left pad, pin, gap, right pad\nconst labelFont = '-apple-system,BlinkMacSystemFont,\"Inter\",\"Segoe UI\",Helvetica,Arial,sans-serif'\nconst placeholder = 'Landmark'\nconst defaultSize = { w: 480, h: 320 }\n// Generous: the outline must clear tool chrome floating outside wrapped shapes (browser URL pill, image prompt) and leave room for the chip.\nconst selectionPadding = 120\n\nexport interface LandmarkProps { w: number; h: number; label: string }\ndeclare module '@tldraw/tlschema' { interface TLGlobalShapePropsMap { landmark: LandmarkProps } }\nexport type LandmarkShape = TLBaseShape<'landmark', LandmarkProps>\n\nexport const landmarkIcon = <MapPin size={24} aria-hidden />\n\n// Wrap the selection (or the viewport centre when nothing is selected) and start naming it.\nexport function createLandmark(editor: Editor) {\n\tif (editor.getIsReadonly()) return\n\tconst selection = editor.getSelectionPageBounds()\n\tconst viewport = editor.getViewportPageBounds()\n\tconst box = selection\n\t\t? { x: selection.x - selectionPadding, y: selection.y - selectionPadding, w: selection.w + selectionPadding * 2, h: selection.h + selectionPadding * 2 }\n\t\t: { x: viewport.midX - defaultSize.w / 2, y: viewport.midY - defaultSize.h / 2, ...defaultSize }\n\tconst id = createShapeId()\n\teditor.markHistoryStoppingPoint('create landmark')\n\teditor.createShape({ id, type: 'landmark', x: box.x, y: box.y, props: { w: box.w, h: box.h, label: '' } })\n\teditor.sendToBack([id])\n\teditor.select(id)\n\teditor.setEditingShape(id)\n}\n\nconst widths = new WeakMap<LandmarkProps, number>()\nfunction chipWidth(editor: Editor, shape: LandmarkShape) {\n\tlet width = widths.get(shape.props)\n\tif (width === undefined) {\n\t\twidth = editor.textMeasure.measureText(shape.props.label || placeholder, {\n\t\t\tfontFamily: labelFont, fontSize: 12, fontWeight: '500', fontStyle: 'normal', lineHeight: 1, maxWidth: null, padding: '0px',\n\t\t}).w + chipPadding\n\t\twidths.set(shape.props, width)\n\t}\n\treturn width\n}\n\nfunction LandmarkShapeView({ shape, editor }: { shape: LandmarkShape; editor: Editor }) {\n\tconst { editing, readonly, finish } = useShapeEditing(editor, shape)\n\tconst zoom = useValue('landmark zoom', () => editor.getEfficientZoomLevel(), [editor])\n\n\treturn (\n\t\t<HTMLContainer className=\"overflow-visible font-sans text-xs/none font-medium text-foreground antialiased\" style={{ width: shape.props.w, height: shape.props.h }}>\n\t\t\t<svg className=\"pointer-events-none absolute inset-0 size-full overflow-visible\" aria-hidden>\n\t\t\t\t<rect width={shape.props.w} height={shape.props.h} className=\"fill-none stroke-foreground/45 [stroke-dasharray:6_4] [stroke-width:1] [vector-effect:non-scaling-stroke]\" />\n\t\t\t</svg>\n\t\t\t<form\n\t\t\t\tclassName={cn('pointer-events-none absolute bottom-full left-0 box-border flex origin-bottom-left items-center gap-1.5 whitespace-nowrap rounded-full pr-2.5 pl-2 text-foreground', editing ? 'pointer-events-auto bg-well shadow-well' : 'bg-key shadow-key')}\n\t\t\t\tstyle={{ height: chipHeight, transform: `scale(${1 / zoom}) translateY(${-chipGap}px)` }}\n\t\t\t\tonPointerDown={editing ? (event) => event.stopPropagation() : undefined}\n\t\t\t\tonSubmit={(event) => { event.preventDefault(); finish() }}\n\t\t\t>\n\t\t\t\t<MapPin className=\"shrink-0\" size={14} aria-hidden />\n\t\t\t\t{editing ? (\n\t\t\t\t\t<ToolChromeField\n\t\t\t\t\t\tclassName=\"after:min-h-7 after:p-0 after:leading-7\" controlClassName=\"min-h-7 p-0 leading-7\"\n\t\t\t\t\t\tvalue={shape.props.label} placeholder={placeholder} aria-label=\"Landmark name\" maxLength={200} disabled={readonly}\n\t\t\t\t\t\tfocus={!readonly} selectOnFocus onEscape={finish}\n\t\t\t\t\t\tonChange={(event) => editor.updateShape({ id: shape.id, type: 'landmark', props: { label: event.currentTarget.value } })}\n\t\t\t\t\t/>\n\t\t\t\t) : <span className={shape.props.label ? undefined : 'text-muted-foreground'}>{shape.props.label || placeholder}</span>}\n\t\t\t</form>\n\t\t</HTMLContainer>\n\t)\n}\n\nexport class LandmarkShapeUtil extends ShapeUtil<LandmarkShape> {\n\tstatic override type = 'landmark' as const\n\tstatic override props: RecordProps<LandmarkShape> = { w: T.number, h: T.number, label: T.string }\n\tgetDefaultProps(): LandmarkProps { return { ...defaultSize, label: '' } }\n\toverride canEdit() { return true }\n\toverride canResize() { return true }\n\toverride hideRotateHandle() { return true }\n\toverride getText(shape: LandmarkShape) { return shape.props.label }\n\t// Frame-like hit-testing reaches the chip outside the bounds and needs full-brush selection; children are never accepted, so nothing reparents into a landmark.\n\toverride isFrameLike() { return true }\n\tgetGeometry(shape: LandmarkShape) {\n\t\tconst z = this.editor.getEfficientZoomLevel()\n\t\treturn new Group2d({ children: [\n\t\t\tnew Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: false }),\n\t\t\tnew Rectangle2d({ x: 0, y: -(chipHeight + chipGap) / z, width: chipWidth(this.editor, shape) / z, height: chipHeight / z, isFilled: true, isLabel: true, excludeFromShapeBounds: true }),\n\t\t] })\n\t}\n\tcomponent(shape: LandmarkShape) { return <LandmarkShapeView shape={shape} editor={this.editor} /> }\n\toverride getIndicatorPath(shape: LandmarkShape) {\n\t\tconst path = new Path2D()\n\t\tpath.rect(0, 0, shape.props.w, shape.props.h)\n\t\treturn path\n\t}\n\toverride onResize(shape: LandmarkShape, info: TLResizeInfo<LandmarkShape>) { return resizeBox(shape, info, { minWidth: 40, minHeight: 40 }) }\n\toverride toSvg(shape: LandmarkShape) {\n\t\treturn (\n\t\t\t<g fill=\"none\" stroke=\"#8a8a8a\">\n\t\t\t\t<rect width={shape.props.w} height={shape.props.h} strokeWidth={1} strokeDasharray=\"6 4\" />\n\t\t\t\t{shape.props.label ? <text x={0} y={-chipGap} fill=\"#8a8a8a\" stroke=\"none\" fontFamily=\"Inter, sans-serif\" fontSize={12} fontWeight={500}>{shape.props.label}</text> : null}\n\t\t\t</g>\n\t\t)\n\t}\n}\n\nexport class LandmarkShapeTool extends EditingBoxShapeTool {\n\tstatic override id = 'landmark'\n\tstatic override initial = 'idle'\n\toverride shapeType = 'landmark' as const\n}\n\nexport const tool: ModTool = { id: 'landmark', label: 'Landmark', icon: landmarkIcon }\nexport const commands: ModCommand[] = [{ id: 'create-landmark', label: 'Create landmark', icon: landmarkIcon, run: createLandmark }]\nexport default (({ config }) => {\n\tconfig.shapeUtils.push(LandmarkShapeUtil)\n\tconfig.tools.push(LandmarkShapeTool)\n}) satisfies ModConfig\n",
      "type": "registry:component",
      "target": "~/src/mods/landmark.tsx"
    }
  ],
  "docs": "Installed to src/mods/. Run npm run apply to load it into the open document.",
  "categories": [
    "shape"
  ],
  "type": "registry:component"
}