diff --git a/apps/examples/src/examples/custom-config/CustomConfigExample.tsx b/apps/examples/src/examples/custom-config/CustomConfigExample.tsx
index 4dde422e7..095186dd3 100644
--- a/apps/examples/src/examples/custom-config/CustomConfigExample.tsx
+++ b/apps/examples/src/examples/custom-config/CustomConfigExample.tsx
@@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { CardShapeTool } from './CardShape/CardShapeTool'
import { CardShapeUtil } from './CardShape/CardShapeUtil'
-import { uiOverrides } from './ui-overrides'
+import { components, uiOverrides } from './ui-overrides'
// There's a guide at the bottom of this file!
@@ -21,6 +21,8 @@ export default function CustomConfigExample() {
tools={customTools}
// Pass in any overrides to the user interface
overrides={uiOverrides}
+ // Pass in the new Keybaord Shortcuts component
+ components={components}
/>
)
diff --git a/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx b/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx
new file mode 100644
index 000000000..ed1fcfeb7
--- /dev/null
+++ b/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx
@@ -0,0 +1,54 @@
+import { Tldraw } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+import { CatDogTool } from './my-shape/my-shape-tool'
+import { CatDogUtil } from './my-shape/my-shape-util'
+import { components, uiOverrides } from './ui-overrides'
+
+// [1]
+const customShapeUtils = [CatDogUtil]
+const customTools = [CatDogTool]
+
+//[2]
+export default function EditableShapeExample() {
+ return (
+
+
+
+ )
+}
+
+/*
+Introduction:
+
+In Tldraw shapes can exist in an editing state. When shapes are in the editing state
+they are focused and can't be dragged, resized or rotated. Shapes enter this state
+when they are double-clicked, this means that users can drag and resize shapes without
+accidentally entering the editing state. In our default shapes we mostly use this for
+editing text, but it's also used in our video shape. In this example we'll create a
+shape that you could use for a game of Go, but instead of black and white stones, we'll
+use cats and dogs.
+
+Most of the relevant code for this is in the my-shape-util.tsx file. We also define a
+very simple tool in my-shape-tool.tsx, and make our new tool appear on the toolbar in
+ui-overrides.ts.
+
+[1]
+We have to define our array of custom shapes and tools outside of the component. So it
+doesn't get redefined every time the component re-renders. We'll pass that in to the
+editors props.
+
+[2]
+We pass in our custom shape classes to the Tldraw component as props. We also pass in
+any uiOverrides we want to use, this is to make sure that our custom tool appears on
+the toolbar.
+
+ */
diff --git a/apps/examples/src/examples/editable-shape/README.md b/apps/examples/src/examples/editable-shape/README.md
new file mode 100644
index 000000000..b3170b8e6
--- /dev/null
+++ b/apps/examples/src/examples/editable-shape/README.md
@@ -0,0 +1,12 @@
+---
+title: Editable Shape
+component: ./EditableShapeExample.tsx
+category: shapes/tools
+priority: 1
+---
+
+A custom shape that you can edit by double-clicking it.
+
+---
+
+Learn how you can have a shape that enters an editing state, and have a side effect run when editing has finished.
diff --git a/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx b/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx
new file mode 100644
index 000000000..bb1eac667
--- /dev/null
+++ b/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx
@@ -0,0 +1,15 @@
+import { BaseBoxShapeTool } from '@tldraw/tldraw'
+export class CatDogTool extends BaseBoxShapeTool {
+ static override id = 'catdog'
+ static override initial = 'idle'
+ override shapeType = 'catdog'
+}
+
+/*
+This file contains our custom tool. The tool is a StateNode with the `id` "catdog".
+
+We get a lot of functionality for free by extending the BaseBoxShapeTool. but we can
+handle events in our own way by overriding methods like onDoubleClick. For an example
+of a tool with more custom functionality, check out the screenshot-tool example.
+
+*/
diff --git a/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx b/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx
new file mode 100644
index 000000000..615a24f8f
--- /dev/null
+++ b/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx
@@ -0,0 +1,196 @@
+/* eslint-disable react-hooks/rules-of-hooks */
+import {
+ HTMLContainer,
+ Rectangle2d,
+ ShapeProps,
+ ShapeUtil,
+ T,
+ TLBaseShape,
+ TLOnEditEndHandler,
+ TLOnResizeHandler,
+ resizeBox,
+ structuredClone,
+ useIsEditing,
+} from '@tldraw/tldraw'
+import { useState } from 'react'
+
+// There's a guide at the bottom of this file!
+
+// [1]
+type ICatDog = TLBaseShape<
+ 'catdog',
+ {
+ w: number
+ h: number
+ }
+>
+
+export class CatDogUtil extends ShapeUtil {
+ // [2]
+ static override type = 'catdog' as const
+ static override props: ShapeProps = {
+ w: T.number,
+ h: T.number,
+ }
+
+ // [3]
+ override isAspectRatioLocked = (_shape: ICatDog) => true
+ override canResize = (_shape: ICatDog) => true
+ override canBind = (_shape: ICatDog) => true
+
+ // [4]
+ override canEdit = () => true
+
+ // [5]
+ getDefaultProps(): ICatDog['props'] {
+ return {
+ w: 170,
+ h: 165,
+ }
+ }
+
+ // [6]
+ getGeometry(shape: ICatDog) {
+ return new Rectangle2d({
+ width: shape.props.w,
+ height: shape.props.h,
+ isFilled: true,
+ })
+ }
+
+ // [7]
+ component(shape: ICatDog) {
+ // [a]
+ const isEditing = useIsEditing(shape.id)
+
+ const [animal, setAnimal] = useState(true)
+
+ // [b]
+ return (
+
+
+
+
{animal ? '🐶' : '🐱'}
+
+
+ )
+ }
+
+ // [8]
+ indicator(shape: ICatDog) {
+ const isEditing = useIsEditing(shape.id)
+ return
+ }
+
+ // [9]
+ override onResize: TLOnResizeHandler = (shape, info) => {
+ return resizeBox(shape, info)
+ }
+
+ // [10]
+ override onEditEnd: TLOnEditEndHandler = (shape) => {
+ const frame1 = structuredClone(shape)
+ const frame2 = structuredClone(shape)
+
+ frame1.x = shape.x + 1.2
+ frame2.x = shape.x - 1.2
+
+ this.editor.animateShape(frame1, { duration: 50 })
+
+ setTimeout(() => {
+ this.editor.animateShape(frame2, { duration: 50 })
+ }, 100)
+
+ setTimeout(() => {
+ this.editor.animateShape(shape, { duration: 100 })
+ }, 200)
+ }
+}
+
+/*
+This is a utility class for the catdog shape. This is where you define the shape's behavior,
+how it renders (its component and indicator), and how it handles different events.
+
+[1]
+This is where we define the shape's type for Typescript. We can extend the TLBaseShape type,
+providing a unique string to identify the shape and the shape's props. We only need height
+and width for this shape.
+
+[2]
+We define the shape's type and props for the editor. We can use tldraw's validator library to
+define the shape's properties. In this case, we define the width and height properties as numbers.
+
+[3]
+Some methods we can override to define specific beahviour for the shape. For this shape, we don't
+want the aspect ratio to change, we want it to resize, and sure it can bind, why not. Who doesn't
+love arrows?
+
+[4]
+This is the important one. We set canEdit to true. This means that the shape can enter the editing
+state.
+
+[5]
+This will be the default props for the shape when you create it via clicking.
+
+[6]
+We define the getGeometry method. This method returns the geometry of the shape. In this case,
+a Rectangle2d object.
+
+[7]
+We define the component method. This controls what the shape looks like and it returns JSX.
+
+ [a] We can use the useIsEditing hook to check if the shape is in the editing state. If it is,
+ we want our shape to render differently.
+
+ [b] The HTML container is a really handy wrapper for custom shapes, it essentially creates a
+ div with some helpful css for you. We can use the isEditing variable to conditionally
+ render the shape. We also use the useState hook to toggle between a cat and a dog.
+
+[8]
+The indicator method is the blue box that appears around the shape when it's selected. We can
+make it appear red if the shape is in the editing state by using the useIsEditing hook.
+
+[9]
+The onResize method is where we handle the resizing of the shape. We use the resizeBox helper
+to handle the resizing for us.
+
+[10]
+The onEditEnd method is called when the shape exits the editing state. In the tldraw codebase we
+mostly use this for trimming text fields in shapes. In this case, we use it to animate the shape
+when it exits the editing state.
+
+*/
diff --git a/apps/examples/src/examples/editable-shape/ui-overrides.tsx b/apps/examples/src/examples/editable-shape/ui-overrides.tsx
new file mode 100644
index 000000000..a37d8e304
--- /dev/null
+++ b/apps/examples/src/examples/editable-shape/ui-overrides.tsx
@@ -0,0 +1,60 @@
+import {
+ DefaultKeyboardShortcutsDialog,
+ DefaultKeyboardShortcutsDialogContent,
+ TLComponents,
+ TLUiOverrides,
+ TldrawUiMenuItem,
+ toolbarItem,
+ useTools,
+} from '@tldraw/tldraw'
+
+// There's a guide at the bottom of this file!
+
+export const uiOverrides: TLUiOverrides = {
+ tools(editor, tools) {
+ // Create a tool item in the ui's context.
+ tools.catdog = {
+ id: 'catdog',
+ icon: 'color',
+ label: 'Catdog',
+ kbd: 'c',
+ onSelect: () => {
+ editor.setCurrentTool('catdog')
+ },
+ }
+ return tools
+ },
+ toolbar(_app, toolbar, { tools }) {
+ // Add the tool item from the context to the toolbar.
+ toolbar.splice(4, 0, toolbarItem(tools.catdog))
+ return toolbar
+ },
+}
+
+export const components: TLComponents = {
+ KeyboardShortcutsDialog: (props) => {
+ const tools = useTools()
+ return (
+
+
+ {/* Ideally, we'd interleave this into the tools group */}
+
+
+ )
+ },
+}
+
+/*
+
+This file contains overrides for the Tldraw UI. These overrides are used to add your custom tools
+to the toolbar and the keyboard shortcuts menu.
+
+We do this by providing a custom toolbar override to the Tldraw component. This override is a
+function that takes the current editor, the default toolbar items, and the default tools.
+It returns the new toolbar items. We use the toolbarItem helper to create a new toolbar item
+for our custom tool. We then splice it into the toolbar items array at the 4th index. This puts
+it after the eraser tool. We'll pass our overrides object into the Tldraw component's `overrides`
+prop.
+
+
+*/