--- title: Click detection created_at: 12/20/2025 updated_at: 12/29/2025 keywords: - click - double-click - overflow click - pointer - ClickManager status: published date: 12/20/2025 readability: 9 voice: 9 completeness: 9 accuracy: 10 notes: Could add a simpler opening hook per voice guide ("Have five minutes?"-style). --- In tldraw, the click detection system turns a pair of nearby clicks into a `double_click` event. The [ClickManager](?) tracks consecutive pointer downs with a small state machine and emits `double_click` when the timing and distance thresholds are met. After a double-click, extra nearby clicks are treated as overflow. Overflow clicks do not dispatch additional click events; they suppress the sequence long enough to prevent a rapid double-double-click from becoming two double-clicks. ## How it works When a pointer down event occurs, the manager either starts a new sequence, detects a double-click, or moves the sequence into overflow. Each state has a timeout that determines how long to wait before returning to idle. Two timeout durations control the detection speed. The first click uses `doubleClickDurationMs` (450ms by default), which is how long the user has to make the second click. After a double-click, `multiClickDurationMs` (200ms by default) controls both the settle delay and the overflow suppression window. The option name is historical; it now controls only the post-double-click window. ### State transitions The click state machine progresses through these states: | State | Description | | ----------------- | --------------------------------------------- | | `idle` | No active click sequence | | `pendingDouble` | First click registered, waiting for second | | `pendingOverflow` | Double-click registered, waiting for overflow | | `overflow` | Extra clicks detected after the double-click | The second pointer down still reaches the state chart as a `pointer_down`, followed immediately by a `double_click` event, and the manager starts waiting for overflow. If the timeout expires before another click, the manager dispatches a double-click settle event and returns to idle. If another pointer down arrives first, the sequence moves to overflow and no further click events are dispatched until the overflow timeout expires. ### Distance validation Consecutive clicks must occur within a maximum distance of 40 pixels (screen space). If pointer down events are farther apart, the new pointer down starts a fresh click sequence instead. ### Click event phases Each click event carries a `phase` that says when in the sequence it fired: | Phase | When it fires | | ------------- | ------------------------------------------------------------------- | | `down` | On the second pointer down, when the double-click is detected | | `up` | On the matching pointer up while the double-click is still pending | | `settle-down` | When the timeout expires without overflow while the pointer is down | | `settle-up` | When the timeout expires without overflow after the pointer is up | The phase system lets tools respond at different points in the click sequence. Most default selection and cropping behavior responds on the `down` phase, so it starts on the second pointer down. The hand tool's double-click zoom responds on `settle-up`, so an overflow click can still cancel the pending zoom. ### Movement cancellation If the pointer moves too far during a pending click sequence, the system cancels the sequence and returns to idle. This prevents double-click detection during click-drag operations. The movement threshold is `dragDistanceSquared` for fine pointers (mouse, stylus) and `coarseDragDistanceSquared` for coarse pointers (touchscreens), both editor [options](/sdk-features/options). The manager only sees pointer events that match the current pen mode: in pen mode, finger taps don't produce double-clicks, and outside pen mode, pen taps don't. ## Handling click events Tools receive click events through handler methods defined in the [TLEventHandlers](?) interface. Here's a complete example of a custom tool that zooms in as soon as a double-click is detected: ```tsx import { StateNode, TLClickEventInfo, Tldraw } from 'tldraw' import 'tldraw/tldraw.css' class ZoomTool extends StateNode { static override id = 'zoom' override onDoubleClick(info: TLClickEventInfo) { if (info.phase !== 'down') return // info.point is in client space; zoomIn wants a point relative to the viewport const screenPoint = this.editor.inputs.getCurrentScreenPoint() this.editor.zoomIn(screenPoint, { animation: { duration: 200 } }) } } const customTools = [ZoomTool] export default function App() { return (