@tldraw/sync 5.2.0-canary.7e69fa3c70ec → 5.2.0-canary.c0d1b9467a7f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS.md ADDED
@@ -0,0 +1,637 @@
1
+ # @tldraw/sync
2
+
3
+ The `@tldraw/sync` package provides React hooks and utilities for integrating real-time collaboration into your tldraw applications. Built on top of `@tldraw/sync-core`, it offers a developer-friendly API that enables multiplayer functionality with minimal configuration.
4
+
5
+ ## 1. Introduction
6
+
7
+ Real-time collaboration transforms single-user drawing applications into shared creative spaces where multiple users can work together simultaneously. The sync package handles the complex coordination required for multiplayer experiences: synchronizing changes, managing user presence, handling network interruptions, and resolving conflicts.
8
+
9
+ You create collaborative tldraw applications using two main hooks:
10
+
11
+ - `useSync` - For production applications with custom servers
12
+ - `useSyncDemo` - For prototypes and demos using tldraw's hosted demo server
13
+
14
+ Both hooks return a store wrapped with connection status, allowing you to build responsive UIs that gracefully handle loading states, connection issues, and real-time updates.
15
+
16
+ ## 2. Core Concepts
17
+
18
+ ### Multiplayer Store State
19
+
20
+ The foundation of sync integration is the **RemoteTLStoreWithStatus**, an enhanced store wrapper that tracks both your drawing data and connection state:
21
+
22
+ ```ts
23
+ import { useSync } from '@tldraw/sync'
24
+
25
+ function MyApp() {
26
+ const store = useSync({
27
+ uri: 'wss://myserver.com/sync/room-123',
28
+ assets: myAssetStore,
29
+ userInfo: { id: 'user-1', name: 'Alice', color: '#ff0000' }
30
+ })
31
+
32
+ // Store progresses through these states:
33
+ if (store.status === 'loading') return <div>Connecting...</div>
34
+ if (store.status === 'error') return <div>Connection failed: {store.error.message}</div>
35
+
36
+ // store.status === 'synced-remote'
37
+ return <Tldraw store={store.store} />
38
+ }
39
+ ```
40
+
41
+ The store moves through three distinct states as it establishes and maintains connection:
42
+
43
+ 1. **loading** - Initial connection and synchronization with the server
44
+ 2. **synced-remote** - Successfully connected and actively synchronizing changes
45
+ 3. **error** - Connection failed or a synchronization error occurred
46
+
47
+ ### User Presence
48
+
49
+ **/User presence** encompasses the real-time information about other users in your collaborative session. This includes cursor positions, current selections, and any custom presence data you want to share. The `useSync` hook handles this automatically, but you can provide a custom `getUserPresence` function to send additional data.
50
+
51
+ ```ts
52
+ const store = useSync({
53
+ uri: wsUri,
54
+ assets: myAssets,
55
+ })
56
+ ```
57
+
58
+ The presence system automatically optimizes itself based on room occupancy, switching between 'solo' mode when you're alone and 'full' mode when collaborating with others.
59
+
60
+ ### Asset Management
61
+
62
+ **Assets** are files like images, videos, and other media that users embed in their drawings. The sync package requires an asset store implementation that handles uploading files and resolving them for display:
63
+
64
+ ```ts
65
+ const myAssetStore = {
66
+ upload: async (asset, file) => {
67
+ // Upload file to your storage service
68
+ const url = await uploadToStorage(file)
69
+ return { src: url }
70
+ },
71
+
72
+ resolve: (asset, context) => {
73
+ // Return optimized URLs based on context
74
+ // (screen DPI, network quality, display size)
75
+ return getOptimizedUrl(asset.src, context)
76
+ },
77
+ }
78
+ ```
79
+
80
+ ## 3. Basic Usage
81
+
82
+ ### Quick Start with Demo Server
83
+
84
+ The fastest way to add collaboration to your application is using the demo server. This requires no backend setup and works immediately:
85
+
86
+ ```ts
87
+ import { useSyncDemo } from '@tldraw/sync'
88
+ import { Tldraw } from 'tldraw'
89
+
90
+ function CollaborativeApp() {
91
+ const store = useSyncDemo({
92
+ roomId: 'my-prototype-room-123',
93
+ userInfo: {
94
+ id: 'user-' + Math.random(),
95
+ name: 'Anonymous User',
96
+ color: '#' + Math.floor(Math.random()*16777215).toString(16)
97
+ }
98
+ })
99
+
100
+ if (store.status === 'loading') {
101
+ return <div>Connecting to room...</div>
102
+ }
103
+
104
+ if (store.status === 'error') {
105
+ return <div>Failed to connect: {store.error.message}</div>
106
+ }
107
+
108
+ return <Tldraw store={store.store} />
109
+ }
110
+ ```
111
+
112
+ > Tip: The demo server is perfect for prototyping and testing, but data is automatically deleted after ~24 hours and rooms are publicly accessible to anyone with the room ID.
113
+
114
+ ### Production Integration
115
+
116
+ For production applications, use `useSync` with your own WebSocket server:
117
+
118
+ ```ts
119
+ import { useSync } from '@tldraw/sync'
120
+ import { Tldraw } from 'tldraw'
121
+
122
+ function ProductionApp() {
123
+ const store = useSync({
124
+ uri: 'wss://myserver.com/sync/project-collaboration-session',
125
+ userInfo: getCurrentUser(), // Your user system integration
126
+ assets: productionAssetStore, // Your asset storage integration
127
+ })
128
+
129
+ return (
130
+ <div>
131
+ {store.status === 'loading' && <LoadingSpinner />}
132
+ {store.status === 'error' && <ErrorMessage error={store.error} />}
133
+ {store.status === 'synced-remote' && (
134
+ <>
135
+ <ConnectionIndicator status={store.connectionStatus} />
136
+ <Tldraw store={store.store} />
137
+ </>
138
+ )}
139
+ </div>
140
+ )
141
+ }
142
+ ```
143
+
144
+ ## 4. Advanced Configuration
145
+
146
+ ### Dynamic Connection URIs
147
+
148
+ You can provide connection URIs dynamically, which is essential for authentication and room-specific routing:
149
+
150
+ ```ts
151
+ const store = useSync({
152
+ uri: async () => {
153
+ const token = await getAuthToken()
154
+ const roomId = getCurrentRoomId()
155
+ return `wss://myserver.com/sync/${roomId}?token=${token}`
156
+ },
157
+ assets: authenticatedAssetStore,
158
+ userInfo: userSignal, // Can be a reactive signal that updates
159
+ })
160
+ ```
161
+
162
+ When the URI function is async, the sync system waits for it to resolve before attempting connection. This ensures your authentication flow completes before establishing the WebSocket connection.
163
+
164
+ ### Reactive User Information
165
+
166
+ User information can be static or reactive. Using reactive signals allows the presence system to automatically update when user details change:
167
+
168
+ ```ts
169
+ import { atom } from '@tldraw/state'
170
+
171
+ const currentUser = atom('currentUser', {
172
+ id: 'user-123',
173
+ name: 'Alice',
174
+ color: '#ff0000',
175
+ })
176
+
177
+ const store = useSync({
178
+ uri: wsUri,
179
+ assets: myAssets,
180
+ userInfo: currentUser, // Reactive signal
181
+ })
182
+
183
+ // Later, when user updates their profile:
184
+ currentUser.set({
185
+ id: 'user-123',
186
+ name: 'Alice Cooper', // Updated name
187
+ color: '#00ff00', // New color
188
+ })
189
+ // Presence automatically updates for all connected users
190
+ ```
191
+
192
+ ### Custom Presence Data
193
+
194
+ The `getUserPresence` function allows you to include custom presence information beyond the standard cursor and selection data. The function receives the `store` and the current `user` info. It should return an object that conforms to the `TLPresenceStateInfo` type.
195
+
196
+ Note that the `store` object passed to this function is a `TLStore` instance, and does not have an `editor` property. To access editor-specific state like the current tool or cursor position, you will need to find a way to access the `Editor` instance from your component.
197
+
198
+ ```ts
199
+ const store = useSync({
200
+ uri: wsUri,
201
+ assets: myAssets,
202
+ getUserPresence: (store, user) => {
203
+ // This function is called whenever the store changes.
204
+ // You can use it to derive presence information from the store.
205
+ // To get information like cursor position, you may need to
206
+ // find a way to access your <Tldraw /> component's editor instance.
207
+
208
+ return {
209
+ userId: user.id,
210
+ userName: user.name,
211
+ // ... and other properties from TLPresenceStateInfo
212
+ }
213
+ },
214
+ })
215
+ ```
216
+
217
+ ### Asset Store Implementation
218
+
219
+ A complete asset store handles both uploading new files and resolving existing assets for optimal display:
220
+
221
+ ```ts
222
+ const productionAssetStore = {
223
+ upload: async (asset, file) => {
224
+ // Generate unique filename
225
+ const filename = `${Date.now()}-${file.name}`
226
+
227
+ // Upload to your storage service
228
+ const formData = new FormData()
229
+ formData.append('file', file)
230
+ formData.append('filename', filename)
231
+
232
+ const response = await fetch('/api/upload', {
233
+ method: 'POST',
234
+ body: formData,
235
+ headers: {
236
+ Authorization: `Bearer ${await getAuthToken()}`,
237
+ },
238
+ })
239
+
240
+ const { url } = await response.json()
241
+ return { src: url }
242
+ },
243
+
244
+ resolve: (asset, context) => {
245
+ const baseUrl = asset.src
246
+
247
+ // Return different resolutions based on context
248
+ if (context.shouldResolveToOriginal) {
249
+ return baseUrl // Full quality for printing/export
250
+ }
251
+
252
+ // Optimize based on display size and screen density
253
+ const targetWidth = Math.ceil(context.screenScale * context.imageSize.w)
254
+ const targetHeight = Math.ceil(context.screenScale * context.imageSize.h)
255
+
256
+ return `${baseUrl}?w=${targetWidth}&h=${targetHeight}&q=${context.networkQuality}`
257
+ },
258
+ }
259
+ ```
260
+
261
+ ## 5. Connection Management
262
+
263
+ ### Understanding Connection States
264
+
265
+ The sync system provides granular connection status information to help you build responsive UIs:
266
+
267
+ ```ts
268
+ function ConnectionAwareApp() {
269
+ const store = useSync({ /* ... */ })
270
+
271
+ if (store.status === 'loading') {
272
+ return <div>Establishing connection...</div>
273
+ }
274
+
275
+ if (store.status === 'error') {
276
+ return (
277
+ <div>
278
+ <h3>Connection Error</h3>
279
+ <p>{store.error.message}</p>
280
+ <button onClick={() => window.location.reload()}>
281
+ Retry Connection
282
+ </button>
283
+ </div>
284
+ )
285
+ }
286
+
287
+ // store.status === 'synced-remote'
288
+ return (
289
+ <div>
290
+ <NetworkIndicator status={store.connectionStatus} />
291
+ <Tldraw store={store.store} />
292
+ </div>
293
+ )
294
+ }
295
+
296
+ function NetworkIndicator({ status }) {
297
+ if (status === 'offline') {
298
+ return <div className="warning">Working offline - changes will sync when reconnected</div>
299
+ }
300
+
301
+ if (status === 'online') {
302
+ return <div className="success">Connected and syncing</div>
303
+ }
304
+
305
+ return null
306
+ }
307
+ ```
308
+
309
+ The connection status operates independently of the sync status. Even when `store.status` is 'synced-remote', the `connectionStatus` can be 'offline' if network connectivity is lost, allowing the application to continue working locally.
310
+
311
+ ### Automatic Reconnection
312
+
313
+ The sync system handles network interruptions gracefully with automatic reconnection and state recovery:
314
+
315
+ ```ts
316
+ // No additional code needed - reconnection is automatic
317
+ const store = useSync({
318
+ uri: 'wss://myserver.com/sync/room-123',
319
+ assets: myAssets,
320
+ userInfo: currentUser,
321
+ })
322
+
323
+ // The system automatically:
324
+ // 1. Detects network disconnection
325
+ // 2. Queues local changes while offline
326
+ // 3. Attempts reconnection with exponential backoff
327
+ // 4. Reconciles state when connection is restored
328
+ // 5. Handles conflicts between local and server changes
329
+ ```
330
+
331
+ ### Error Handling and Recovery
332
+
333
+ Different types of connection errors require different handling strategies:
334
+
335
+ ```ts
336
+ function ErrorHandlingApp() {
337
+ const store = useSync({ /* ... */ })
338
+
339
+ if (store.status === 'error') {
340
+ const error = store.error
341
+
342
+ // Check specific error types for appropriate responses
343
+ if (error.reason === 'NOT_FOUND') {
344
+ return <div>Room not found. Please check the room ID.</div>
345
+ }
346
+
347
+ if (error.reason === 'FORBIDDEN') {
348
+ return <div>Access denied. Please check your permissions.</div>
349
+ }
350
+
351
+ if (error.reason === 'NOT_AUTHENTICATED') {
352
+ return <div>Authentication required. <button onClick={login}>Login</button></div>
353
+ }
354
+
355
+ if (error.reason === 'RATE_LIMITED') {
356
+ return <div>Too many requests. Please wait before retrying.</div>
357
+ }
358
+
359
+ // Generic network or server error
360
+ return (
361
+ <div>
362
+ <h3>Connection Error</h3>
363
+ <p>Unable to connect to collaboration server.</p>
364
+ <button onClick={() => window.location.reload()}>Retry</button>
365
+ </div>
366
+ )
367
+ }
368
+
369
+ return <Tldraw store={store.store} />
370
+ }
371
+ ```
372
+
373
+ ## 6. Debugging
374
+
375
+ ### Understanding Connection Behavior
376
+
377
+ The sync package provides several tools for understanding and debugging connection behavior in your application.
378
+
379
+ #### Connection Status Monitoring
380
+
381
+ You can monitor connection events by logging the status changes:
382
+
383
+ ```ts
384
+ import { useEffect } from 'react'
385
+
386
+ function DebuggableApp() {
387
+ const store = useSync({
388
+ uri: 'wss://myserver.com/sync/room-123',
389
+ assets: myAssets,
390
+ userInfo: currentUser,
391
+ trackAnalyticsEvent: (name, data) => {
392
+ console.log('Sync Event:', name, data)
393
+ }
394
+ })
395
+
396
+ useEffect(() => {
397
+ console.log('Store status changed:', store.status)
398
+
399
+ if (store.status === 'synced-remote') {
400
+ console.log('Connection status:', store.connectionStatus)
401
+ }
402
+ }, [store.status, store.status === 'synced-remote' ? store.connectionStatus : null])
403
+
404
+ return <Tldraw store={store.store} />
405
+ }
406
+ ```
407
+
408
+ This will log events like:
409
+
410
+ ```
411
+ Sync Event: room-not-found { roomId: "room-123" }
412
+ Sync Event: connected { isReadonly: false }
413
+ Store status changed: synced-remote
414
+ Connection status: online
415
+ ```
416
+
417
+ #### Network Quality Detection
418
+
419
+ The demo asset store includes network quality detection that affects image resolution:
420
+
421
+ ```ts
422
+ // In the demo environment, you can observe network adaptation:
423
+ const store = useSyncDemo({
424
+ roomId: 'debug-room',
425
+ userInfo: currentUser,
426
+ })
427
+
428
+ // Images automatically adjust quality based on:
429
+ // - Connection speed (detected from WebSocket latency)
430
+ // - Screen pixel density
431
+ // - Actual display size
432
+ // - File size thresholds
433
+ ```
434
+
435
+ #### Presence System Debugging
436
+
437
+ You can debug presence updates by monitoring presence mode changes:
438
+
439
+ ```ts
440
+ import { getDefaultUserPresence } from 'tldraw'
441
+
442
+ function PresenceDebuggingApp() {
443
+ const store = useSync({
444
+ uri: wsUri,
445
+ assets: myAssets,
446
+ getUserPresence: (store, user) => {
447
+ // See the "Custom Presence Data" section for details
448
+ // on how to implement this function.
449
+ const presence = getDefaultUserPresence(store, user)
450
+ console.log('Updating presence:', presence)
451
+ return presence
452
+ },
453
+ })
454
+
455
+ useEffect(() => {
456
+ if (store.status === 'synced-remote') {
457
+ // Monitor presence mode changes
458
+ const unsubscribe = store.store.listen(() => {
459
+ const presences = store.store.allRecords().filter(r => r.typeName === 'instance_presence')
460
+ console.log('Active users:', presences.length)
461
+ console.log('Presence mode:', presences.length > 1 ? 'collaborative' : 'solo')
462
+ })
463
+
464
+ return unsubscribe
465
+ }
466
+ }, [store])
467
+
468
+ return <Tldraw store={store.store} />
469
+ }
470
+ ```
471
+
472
+ #### Common Connection Issues
473
+
474
+ **WebSocket Connection Failures**
475
+
476
+ If connections consistently fail, verify the WebSocket URL format:
477
+
478
+ ```ts
479
+ // ✅ Correct formats:
480
+ 'wss://myserver.com/sync'
481
+ 'wss://myserver.com/sync/room-123'
482
+ 'ws://localhost:3001/sync' // Development only
483
+
484
+ // ❌ Common mistakes:
485
+ 'https://myserver.com/sync' // Wrong protocol
486
+ 'wss://myserver.com/sync/' // Trailing slash may cause issues
487
+ 'myserver.com/sync' // Missing protocol
488
+ ```
489
+
490
+ **Authentication Token Issues**
491
+
492
+ When using token authentication, ensure tokens remain valid throughout the session:
493
+
494
+ ```ts
495
+ const store = useSync({
496
+ uri: async () => {
497
+ const token = await refreshTokenIfNeeded() // Ensure token is fresh
498
+ return `wss://myserver.com/sync?token=${token}`
499
+ },
500
+ assets: myAssets,
501
+ userInfo: currentUser,
502
+ })
503
+ ```
504
+
505
+ **Asset Upload Problems**
506
+
507
+ Asset upload failures often relate to CORS configuration or authentication:
508
+
509
+ ```ts
510
+ const debugAssetStore = {
511
+ upload: async (asset, file) => {
512
+ console.log('Uploading asset:', {
513
+ name: file.name,
514
+ size: file.size,
515
+ type: file.type,
516
+ })
517
+
518
+ try {
519
+ const result = await uploadToServer(file)
520
+ console.log('Upload successful:', result)
521
+ return result
522
+ } catch (error) {
523
+ console.error('Upload failed:', error)
524
+ throw error
525
+ }
526
+ },
527
+
528
+ resolve: (asset, context) => {
529
+ console.log('Resolving asset:', asset.src, 'with context:', context)
530
+ return asset.src
531
+ },
532
+ }
533
+ ```
534
+
535
+ ## 7. Integration with Authentication
536
+
537
+ ### Token-Based Authentication
538
+
539
+ Most production applications require authentication. The sync package supports token-based auth through dynamic URI generation:
540
+
541
+ ```ts
542
+ import { useAuth } from './auth-system'
543
+
544
+ function AuthenticatedApp() {
545
+ const { user, getToken } = useAuth()
546
+
547
+ const store = useSync({
548
+ uri: async () => {
549
+ if (!user) throw new Error('Not authenticated')
550
+
551
+ const token = await getToken()
552
+ return `wss://myserver.com/sync/room-123?token=${token}&userId=${user.id}`
553
+ },
554
+ userInfo: {
555
+ id: user.id,
556
+ name: user.displayName,
557
+ color: user.preferredColor
558
+ },
559
+ assets: createAuthenticatedAssetStore(getToken),
560
+ })
561
+
562
+ if (!user) {
563
+ return <LoginPrompt />
564
+ }
565
+
566
+ return <Tldraw store={store.store} />
567
+ }
568
+ ```
569
+
570
+ ### Session Management
571
+
572
+ Handle authentication state changes gracefully by recreating the sync connection:
573
+
574
+ ```ts
575
+ function SessionManagedApp() {
576
+ const { user, sessionId } = useAuth()
577
+
578
+ // Recreate sync connection when session changes
579
+ const store = useSync({
580
+ uri: `wss://myserver.com/sync?session=${sessionId}`,
581
+ userInfo: user ? {
582
+ id: user.id,
583
+ name: user.name,
584
+ color: user.color
585
+ } : null,
586
+ assets: myAssetStore,
587
+ })
588
+
589
+ // Handle logout
590
+ const handleLogout = () => {
591
+ // Sync connection will automatically clean up
592
+ // when component unmounts or deps change
593
+ logout()
594
+ }
595
+
596
+ return (
597
+ <div>
598
+ {user && <button onClick={handleLogout}>Logout</button>}
599
+ <Tldraw store={store.store} />
600
+ </div>
601
+ )
602
+ }
603
+ ```
604
+
605
+ ### Role-Based Permissions
606
+
607
+ Integrate with permission systems by handling readonly mode:
608
+
609
+ ```ts
610
+ function PermissionAwareApp() {
611
+ const store = useSync({
612
+ uri: wsUri,
613
+ assets: myAssets,
614
+ userInfo: currentUser,
615
+ })
616
+
617
+ if (store.status === 'synced-remote') {
618
+ // Server can set readonly mode based on user permissions
619
+ const isReadonly = store.store.collaboration?.mode === 'readonly'
620
+
621
+ if (isReadonly) {
622
+ return (
623
+ <div>
624
+ <div className="notice">You have view-only access to this room</div>
625
+ <Tldraw store={store.store} />
626
+ </div>
627
+ )
628
+ }
629
+ }
630
+
631
+ return <Tldraw store={store.store} />
632
+ }
633
+ ```
634
+
635
+ The sync system automatically handles permission enforcement when the server sets readonly mode, preventing local changes from being applied or synchronized.
636
+
637
+ This comprehensive guide covers the essential concepts, practical implementation patterns, and advanced features of the `@tldraw/sync` package. The reactive nature of the sync system, combined with robust error handling and flexible configuration options, enables you to build reliable collaborative experiences that gracefully handle the complexities of real-time multiplayer interaction.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  This project contains source code for [tldraw sync](https://tldraw.dev/docs/sync). [Click here](https://tldraw.dev/blog/product/announcing-tldraw-sync) to learn more.
4
4
 
5
+ ## Documentation
6
+
7
+ A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
8
+
5
9
  ## License
6
10
 
7
11
  This project is part of the tldraw SDK. It is provided under the [tldraw SDK license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
package/dist-cjs/index.js CHANGED
@@ -29,7 +29,7 @@ var import_useSync = require("./useSync");
29
29
  var import_useSyncDemo = require("./useSyncDemo");
30
30
  (0, import_utils.registerTldrawLibraryVersion)(
31
31
  "@tldraw/sync",
32
- "5.2.0-canary.7e69fa3c70ec",
32
+ "5.2.0-canary.c0d1b9467a7f",
33
33
  "cjs"
34
34
  );
35
35
  //# sourceMappingURL=index.js.map
@@ -6,7 +6,7 @@ import {
6
6
  import { useSyncDemo } from "./useSyncDemo.mjs";
7
7
  registerTldrawLibraryVersion(
8
8
  "@tldraw/sync",
9
- "5.2.0-canary.7e69fa3c70ec",
9
+ "5.2.0-canary.c0d1b9467a7f",
10
10
  "esm"
11
11
  );
12
12
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
4
- "version": "5.2.0-canary.7e69fa3c70ec",
4
+ "version": "5.2.0-canary.c0d1b9467a7f",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -29,7 +29,8 @@
29
29
  "files": [
30
30
  "dist-esm",
31
31
  "dist-cjs",
32
- "src"
32
+ "src",
33
+ "DOCS.md"
33
34
  ],
34
35
  "scripts": {
35
36
  "test-ci": "yarn run -T vitest run --passWithNoTests",
@@ -53,12 +54,12 @@
53
54
  "vitest": "^3.2.4"
54
55
  },
55
56
  "dependencies": {
56
- "@tldraw/state": "5.2.0-canary.7e69fa3c70ec",
57
- "@tldraw/state-react": "5.2.0-canary.7e69fa3c70ec",
58
- "@tldraw/sync-core": "5.2.0-canary.7e69fa3c70ec",
59
- "@tldraw/utils": "5.2.0-canary.7e69fa3c70ec",
57
+ "@tldraw/state": "5.2.0-canary.c0d1b9467a7f",
58
+ "@tldraw/state-react": "5.2.0-canary.c0d1b9467a7f",
59
+ "@tldraw/sync-core": "5.2.0-canary.c0d1b9467a7f",
60
+ "@tldraw/utils": "5.2.0-canary.c0d1b9467a7f",
60
61
  "nanoevents": "^7.0.1",
61
- "tldraw": "5.2.0-canary.7e69fa3c70ec",
62
+ "tldraw": "5.2.0-canary.c0d1b9467a7f",
62
63
  "ws": "^8.18.0"
63
64
  },
64
65
  "peerDependencies": {