<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value=".actorcore
node_modules">
<input type="hidden" name="project[files][README.md]" value="# Real-time Collaborative Cursors for RivetKit

Example project demonstrating real-time cursor tracking and collaborative canvas with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-dev/rivetkit)

[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 18+

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/cursors
npm install
```

### Development

```sh
npm run dev
```

Open your browser to `http://localhost:5173`. Open multiple tabs or windows to see real-time cursor tracking and text placement across different users.

## Features

- Real-time cursor position tracking
- Multiple users with color-coded cursors
- Click-to-place text on canvas
- Multiple room support for different collaborative spaces
- Persistent text labels across sessions
- Event-driven architecture with RivetKit actors
- TypeScript support throughout

## License

Apache 2.0
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-cursors&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;npm run dev:backend\&quot; \&quot;npm run dev:frontend\&quot;&quot;,&quot;dev:backend&quot;:&quot;tsx --watch src/backend/server.ts&quot;,&quot;dev:frontend&quot;:&quot;vite&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;@types/prompts&quot;:&quot;^2&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.2.0&quot;,&quot;concurrently&quot;:&quot;^8.2.2&quot;,&quot;prompts&quot;:&quot;^2.4.2&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.5.2&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@1784d8f4835dfe0620f43e819ff860dd7dd02821&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;},&quot;dependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@1784d8f4835dfe0620f43e819ff860dd7dd02821&quot;},&quot;stableVersion&quot;:&quot;0.8.0&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		/* Visit https://aka.ms/tsconfig.json to read more about this file */

		/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
		&quot;target&quot;: &quot;esnext&quot;,
		/* Specify a set of bundled library declaration files that describe the target runtime environment. */
		&quot;lib&quot;: [&quot;esnext&quot;, &quot;dom&quot;],
		/* Specify what JSX code is generated. */
		&quot;jsx&quot;: &quot;react-jsx&quot;,

		/* Specify what module code is generated. */
		&quot;module&quot;: &quot;esnext&quot;,
		/* Specify how TypeScript looks up a file from a given module specifier. */
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		/* Specify type package names to be included without being referenced in a source file. */
		&quot;types&quot;: [&quot;node&quot;, &quot;vite/client&quot;, &quot;vitest&quot;],
		/* Enable importing .json files */
		&quot;resolveJsonModule&quot;: true,

		/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
		&quot;allowJs&quot;: true,
		/* Enable error reporting in type-checked JavaScript files. */
		&quot;checkJs&quot;: false,

		/* Disable emitting files from a compilation. */
		&quot;noEmit&quot;: true,

		/* Ensure that each file can be safely transpiled without relying on other imports. */
		&quot;isolatedModules&quot;: true,
		/* Allow &#39;import x from y&#39; when a module doesn&#39;t have a default export. */
		&quot;allowSyntheticDefaultImports&quot;: true,
		/* Ensure that casing is correct in imports. */
		&quot;forceConsistentCasingInFileNames&quot;: true,

		/* Enable all strict type-checking options. */
		&quot;strict&quot;: true,

		/* Skip type checking all .d.ts files. */
		&quot;skipLibCheck&quot;: true
	},
	&quot;include&quot;: [&quot;src/**/*&quot;, &quot;actors/**/*&quot;, &quot;tests/**/*&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	root: &quot;src/frontend&quot;,
	server: {
		port: 5173,
	},
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	server: {
		port: 5173,
	},
	test: {
		include: [&quot;tests/**/*.test.ts&quot;],
	},
});
">
<input type="hidden" name="project[files][src/backend/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

export interface CursorPosition {
	userId: string;
	x: number;
	y: number;
	timestamp: number;
}

export interface TextLabel {
	id: string;
	userId: string;
	text: string;
	x: number;
	y: number;
	timestamp: number;
}

export const cursorRoom = actor({
	state: {
		textLabels: [] as TextLabel[],
	},

	connState: {
		cursor: null as CursorPosition | null,
	},

	actions: {
		// Update cursor position
		updateCursor: (c, userId: string, x: number, y: number) =&gt; {
			const cursor: CursorPosition = {
				userId,
				x,
				y,
				timestamp: Date.now(),
			};
			c.conn.state.cursor = cursor;
			c.broadcast(&quot;cursorMoved&quot;, cursor);
			return cursor;
		},

		// Update text on the canvas (creates or updates)
		updateText: (
			c,
			id: string,
			userId: string,
			text: string,
			x: number,
			y: number,
		) =&gt; {
			const textLabel: TextLabel = {
				id,
				userId,
				text,
				x,
				y,
				timestamp: Date.now(),
			};

			// Find and update existing text label or add new one
			const existingIndex = c.state.textLabels.findIndex(
				(label) =&gt; label.id === id,
			);
			if (existingIndex &gt;= 0) {
				c.state.textLabels[existingIndex] = textLabel;
			} else {
				c.state.textLabels.push(textLabel);
			}

			c.broadcast(&quot;textUpdated&quot;, textLabel);
			return textLabel;
		},

		// Remove text from the canvas
		removeText: (c, id: string) =&gt; {
			c.state.textLabels = c.state.textLabels.filter(
				(label) =&gt; label.id !== id,
			);
			c.broadcast(&quot;textRemoved&quot;, id);
		},

		// Get all room state (cursors and text labels)
		getRoomState: (c) =&gt; {
			const cursors: Record&lt;string, CursorPosition&gt; = {};
			for (const conn of c.conns.values()) {
				if (conn.state.cursor) {
					cursors[conn.state.cursor.userId] = conn.state.cursor;
				}
			}
			return {
				cursors,
				textLabels: c.state.textLabels,
			};
		},
	},
});

// Register actors for use: https://rivet.dev/docs/setup
export const registry = setup({
	use: { cursorRoom },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { registry } from &quot;./registry&quot;;

registry.start();
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useRef, useState } from &quot;react&quot;;
import type {
	CursorPosition,
	TextLabel,
	registry,
} from &quot;../backend/registry&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(&quot;http://localhost:6420&quot;);

// Generate a random user ID
const generateUserId = () =&gt;
	`user-${Math.random().toString(36).substring(2, 9)}`;

// Cursor colors for different users (darker palette)
const CURSOR_COLORS = [
	&quot;#E63946&quot;,
	&quot;#2A9D8F&quot;,
	&quot;#1B8AAE&quot;,
	&quot;#F77F00&quot;,
	&quot;#06A77D&quot;,
	&quot;#D4A017&quot;,
	&quot;#9B59B6&quot;,
	&quot;#5DADE2&quot;,
];

function getColorForUser(userId: string): string {
	let hash = 0;
	for (let i = 0; i &lt; userId.length; i++) {
		hash = userId.charCodeAt(i) + ((hash &lt;&lt; 5) - hash);
	}
	return CURSOR_COLORS[Math.abs(hash) % CURSOR_COLORS.length];
}

// Virtual canvas size - all coordinates are in this space
const CANVAS_WIDTH = 1920;
const CANVAS_HEIGHT = 1080;

export function App() {
	const [roomId, setRoomId] = useState(&quot;general&quot;);
	const [userId] = useState(generateUserId());
	const [cursors, setCursors] = useState&lt;Record&lt;string, CursorPosition&gt;&gt;({});
	const [textLabels, setTextLabels] = useState&lt;TextLabel[]&gt;([]);
	const [textInput, setTextInput] = useState(&quot;&quot;);
	const [isTyping, setIsTyping] = useState(false);
	const [typingPosition, setTypingPosition] = useState({ x: 0, y: 0 });
	const [currentTextId, setCurrentTextId] = useState&lt;string | null&gt;(null);
	const [scale, setScale] = useState(1);
	const canvasRef = useRef&lt;HTMLDivElement&gt;(null);
	const containerRef = useRef&lt;HTMLDivElement&gt;(null);

	const cursorRoom = useActor({
		name: &quot;cursorRoom&quot;,
		key: [roomId],
	});

	// Calculate scale factor to fit canvas in viewport
	useEffect(() =&gt; {
		const updateScale = () =&gt; {
			if (!containerRef.current) return;

			const containerWidth = containerRef.current.clientWidth;
			const containerHeight = containerRef.current.clientHeight;

			// Calculate scale to fit canvas while maintaining aspect ratio
			const scaleX = containerWidth / CANVAS_WIDTH;
			const scaleY = containerHeight / CANVAS_HEIGHT;
			const newScale = Math.min(scaleX, scaleY);

			setScale(newScale);
		};

		updateScale();
		window.addEventListener(&quot;resize&quot;, updateScale);
		return () =&gt; window.removeEventListener(&quot;resize&quot;, updateScale);
	}, []);

	// Load initial state
	useEffect(() =&gt; {
		if (cursorRoom.connection) {
			cursorRoom.connection.getRoomState().then((state) =&gt; {
				setCursors(state.cursors);
				setTextLabels(state.textLabels);
			}).catch((error) =&gt; {
				console.error(&#39;error loading room state&#39;, error);
			});
		}
	}, [cursorRoom.connection]);

	// Listen for cursor movements
	cursorRoom.useEvent(&quot;cursorMoved&quot;, (cursor: CursorPosition) =&gt; {
		setCursors((prev) =&gt; ({
			...prev,
			[cursor.userId]: cursor,
		}));
	});

	// Listen for text updates
	cursorRoom.useEvent(&quot;textUpdated&quot;, (label: TextLabel) =&gt; {
		setTextLabels((prev) =&gt; {
			const existingIndex = prev.findIndex(l =&gt; l.id === label.id);
			if (existingIndex &gt;= 0) {
				const newLabels = [...prev];
				newLabels[existingIndex] = label;
				return newLabels;
			} else {
				return [...prev, label];
			}
		});
	});

	// Listen for text removal
	cursorRoom.useEvent(&quot;textRemoved&quot;, (id: string) =&gt; {
		setTextLabels((prev) =&gt; prev.filter(label =&gt; label.id !== id));
	});

	// Listen for cursor removal (when connection closes)
	cursorRoom.useEvent(&quot;cursorRemoved&quot;, (cursor: CursorPosition) =&gt; {
		setCursors((prev) =&gt; {
			const newCursors = { ...prev };
			delete newCursors[cursor.userId];
			return newCursors;
		});
	});

	// Convert screen coordinates to canvas coordinates
	const screenToCanvas = (screenX: number, screenY: number) =&gt; {
		if (!canvasRef.current) return { x: 0, y: 0 };

		const rect = canvasRef.current.getBoundingClientRect();
		const x = (screenX - rect.left) / scale;
		const y = (screenY - rect.top) / scale;

		return { x, y };
	};

	// Handle mouse movement on canvas
	const handleMouseMove = (e: React.MouseEvent&lt;HTMLDivElement&gt;) =&gt; {
		if (cursorRoom.connection &amp;&amp; canvasRef.current) {
			const { x, y } = screenToCanvas(e.clientX, e.clientY);
			cursorRoom.connection.updateCursor(userId, x, y);
		}
	};

	// Handle canvas click
	const handleCanvasClick = (e: React.MouseEvent&lt;HTMLDivElement&gt;) =&gt; {
		if (!canvasRef.current) return;

		const { x, y } = screenToCanvas(e.clientX, e.clientY);
		const newTextId = `${userId}-${Date.now()}`;
		setTypingPosition({ x, y });
		setCurrentTextId(newTextId);
		setIsTyping(true);
		setTextInput(&quot;&quot;);
	};

	// Handle text input changes
	const handleTextChange = (newText: string) =&gt; {
		setTextInput(newText);
		if (cursorRoom.connection &amp;&amp; currentTextId &amp;&amp; newText.trim()) {
			cursorRoom.connection.updateText(
				currentTextId,
				userId,
				newText,
				typingPosition.x,
				typingPosition.y,
			);
		}
	};

	// Handle key press while typing
	const handleKeyDown = (e: React.KeyboardEvent) =&gt; {
		if (e.key === &quot;Enter&quot;) {
			// Finalize the text
			if (textInput.trim() &amp;&amp; cursorRoom.connection &amp;&amp; currentTextId) {
				cursorRoom.connection.updateText(
					currentTextId,
					userId,
					textInput,
					typingPosition.x,
					typingPosition.y,
				);
			} else if (cursorRoom.connection &amp;&amp; currentTextId) {
				// Remove empty text
				cursorRoom.connection.removeText(currentTextId);
			}
			setTextInput(&quot;&quot;);
			setIsTyping(false);
			setCurrentTextId(null);
		} else if (e.key === &quot;Escape&quot;) {
			// Cancel typing and remove text
			if (cursorRoom.connection &amp;&amp; currentTextId) {
				cursorRoom.connection.removeText(currentTextId);
			}
			setTextInput(&quot;&quot;);
			setIsTyping(false);
			setCurrentTextId(null);
		}
	};

	// Cursor is automatically removed when connection closes via connState cleanup

	return (
		&lt;div className=&quot;app-container&quot;&gt;
			&lt;div className=&quot;controls&quot;&gt;
				&lt;div className=&quot;control-group&quot;&gt;
					&lt;label&gt;Room:&lt;/label&gt;
					&lt;input
						type=&quot;text&quot;
						value={roomId}
						onChange={(e) =&gt; setRoomId(e.target.value)}
						placeholder=&quot;Enter room name&quot;
					/&gt;
				&lt;/div&gt;
				&lt;div className=&quot;user-info&quot;&gt;
					Your ID: &lt;span style={{ color: getColorForUser(userId) }}&gt;{userId}&lt;/span&gt;
				&lt;/div&gt;
			&lt;/div&gt;

			&lt;div ref={containerRef} className=&quot;canvas-container&quot;&gt;
				&lt;div
					ref={canvasRef}
					className=&quot;canvas&quot;
					style={{
						width: `${CANVAS_WIDTH}px`,
						height: `${CANVAS_HEIGHT}px`,
						transform: `translate(-50%, -50%) scale(${scale})`,
					}}
					onMouseMove={handleMouseMove}
					onClick={handleCanvasClick}
					tabIndex={0}
					onKeyDown={handleKeyDown}
				&gt;
					{/* Render text labels */}
					{textLabels
						.filter((label) =&gt; label.id !== currentTextId)
						.map((label) =&gt; (
							&lt;div
								key={label.id}
								className=&quot;text-label&quot;
								style={{
									left: label.x,
									top: label.y,
									color: getColorForUser(label.userId),
								}}
							&gt;
								{label.text}
							&lt;/div&gt;
						))}

					{/* Render text being typed */}
					{isTyping &amp;&amp; (
						&lt;div
							className=&quot;typing-container&quot;
							style={{
								left: typingPosition.x,
								top: typingPosition.y,
							}}
						&gt;
							&lt;div
								className=&quot;typing-text&quot;
								style={{
									color: getColorForUser(userId),
								}}
							&gt;
								{textInput}
								&lt;span className=&quot;typing-cursor&quot;&gt;|&lt;/span&gt;
							&lt;/div&gt;
							&lt;div
								className=&quot;enter-hint&quot;
								style={{
									borderColor: getColorForUser(userId),
									color: getColorForUser(userId),
								}}
							&gt;
								enter
							&lt;/div&gt;
						&lt;/div&gt;
					)}

					{/* Render cursors */}
					{Object.entries(cursors).map(([id, cursor]) =&gt; {
						const color = getColorForUser(cursor.userId);
						const isOwnCursor = id === userId;
						return (
							&lt;div
								key={id}
								className=&quot;cursor&quot;
								style={{
									left: cursor.x,
									top: cursor.y,
								}}
							&gt;
								&lt;svg
									width=&quot;20&quot;
									height=&quot;24&quot;
									viewBox=&quot;0 0 20 24&quot;
									fill=&quot;none&quot;
									xmlns=&quot;http://www.w3.org/2000/svg&quot;
									className=&quot;cursor-svg&quot;
								&gt;
									&lt;path
										d=&quot;M10 4 L4 18 L16 18 Z&quot;
										fill={color}
										stroke=&quot;white&quot;
										strokeWidth=&quot;1.5&quot;
										strokeLinecap=&quot;round&quot;
										strokeLinejoin=&quot;round&quot;
										transform=&quot;rotate(-45 10 12)&quot;
									/&gt;
								&lt;/svg&gt;
								&lt;div
									className=&quot;cursor-label&quot;
									style={{
										backgroundColor: color,
										borderColor: `${color}40`,
									}}
								&gt;
									{isOwnCursor ? &quot;you&quot; : cursor.userId}
								&lt;/div&gt;
							&lt;/div&gt;
						);
					})}

					{!cursorRoom.connection &amp;&amp; (
						&lt;div className=&quot;loading-overlay&quot;&gt;Connecting to room...&lt;/div&gt;
					)}

					{/* Hidden input to capture typing */}
					{isTyping &amp;&amp; (
						&lt;input
							type=&quot;text&quot;
							className=&quot;hidden-input&quot;
							value={textInput}
							onChange={(e) =&gt; handleTextChange(e.target.value)}
							onBlur={() =&gt; {
								if (!textInput.trim() &amp;&amp; cursorRoom.connection &amp;&amp; currentTextId) {
									cursorRoom.connection.removeText(currentTextId);
									setCurrentTextId(null);
								}
								setIsTyping(false);
							}}
							autoFocus
						/&gt;
					)}
				&lt;/div&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][src/frontend/index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Cursors Example&lt;/title&gt;
    &lt;style&gt;
        * {
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #0a0a0a;
            color: #e0e0e0;
            overflow: hidden;
            height: 100vh;
        }
        .app-container {
            position: relative;
            width: 100vw;
            height: 100vh;
        }
        .controls {
            position: absolute;
            top: 20px;
            left: 20px;
            right: 20px;
            z-index: 200;
            background: rgba(26, 26, 26, 0.9);
            backdrop-filter: blur(10px);
            padding: 16px 20px;
            border-radius: 8px;
            border: 1px solid #2a2a2a;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
        }
        .control-group {
            display: flex;
            gap: 10px;
            align-items: center;
        }
        .control-group label {
            font-weight: 500;
            color: #a0a0a0;
        }
        .control-group input {
            padding: 8px 12px;
            border: 1px solid #333;
            border-radius: 4px;
            min-width: 200px;
            background: #2a2a2a;
            color: #e0e0e0;
        }
        .control-group input:focus {
            outline: none;
            border-color: #4a9eff;
        }
        .user-info {
            font-size: 14px;
            color: #808080;
        }
        .canvas-container {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            overflow: hidden;
            background: #0a0a0a;
        }
        .canvas {
            position: absolute;
            top: 50%;
            left: 50%;
            transform-origin: center center;
            background: #1a1a1a;
            cursor: none;
            overflow: hidden;
        }
        .cursor {
            position: absolute;
            pointer-events: none;
            z-index: 100;
        }
        .cursor-svg {
            filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
        }
        .cursor-label {
            position: absolute;
            top: 20px;
            left: 18px;
            padding: 4px 8px;
            border-radius: 8px;
            color: white;
            font-size: 12px;
            white-space: nowrap;
            border: 1px solid;
        }
        .text-label {
            position: absolute;
            font-size: 16px;
            font-weight: 500;
            pointer-events: none;
            white-space: nowrap;
            transform: translate(0, -50%);
            text-shadow: 0 2px 4px rgba(0,0,0,0.5);
            line-height: 1;
        }
        .typing-container {
            position: absolute;
            display: flex;
            align-items: center;
            gap: 8px;
            pointer-events: none;
            transform: translate(0, -50%);
        }
        .typing-text {
            font-size: 16px;
            font-weight: 500;
            white-space: nowrap;
            text-shadow: 0 2px 4px rgba(0,0,0,0.5);
            line-height: 1;
        }
        .typing-cursor {
            animation: blink 1s infinite;
            margin-left: 2px;
            display: inline-block;
            position: relative;
            top: -2px;
        }
        @keyframes blink {
            0%, 50% { opacity: 1; }
            51%, 100% { opacity: 0; }
        }
        .enter-hint {
            font-size: 11px;
            font-weight: 500;
            white-space: nowrap;
            padding: 2px 6px;
            border: 1px solid;
            border-radius: 4px;
            background: rgba(0, 0, 0, 0.5);
            opacity: 0.7;
        }
        .hidden-input {
            position: absolute;
            top: -9999px;
            left: -9999px;
            width: 1px;
            height: 1px;
            opacity: 0;
            pointer-events: none;
        }
        .loading-overlay {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            font-size: 18px;
            color: #808080;
        }
        .instructions {
            background: #1a1a1a;
            padding: 20px;
            border-radius: 8px;
            border: 1px solid #2a2a2a;
            margin: 20px;
        }
        .instructions h4 {
            margin-top: 0;
            color: #4a9eff;
        }
        .instructions ul {
            margin: 0;
            padding-left: 20px;
        }
        .instructions li {
            margin-bottom: 8px;
            color: #a0a0a0;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;">
<input type="hidden" name="project[files][src/frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="example-cursors">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>