{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workspace",
  "title": "Workspace",
  "description": "Document-script workspace for tldraw offline: esbuild + Tailwind build, apply-to-open-document, and a shell that loads every mod in src/mods/.",
  "dependencies": [
    "cn",
    "lucide-react",
    "tw-animate-css",
    "shadcn",
    "@base-ui/react"
  ],
  "devDependencies": [
    "esbuild",
    "tailwindcss",
    "@tailwindcss/node",
    "@tailwindcss/oxide",
    "typescript"
  ],
  "files": [
    {
      "path": "workspace/build.mjs",
      "content": "import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { build } from 'esbuild'\nimport { tailwindPlugin } from './tailwind-plugin.mjs'\n\nconst root = dirname(fileURLToPath(import.meta.url))\nconst src = join(root, 'src')\nconst modsDir = join(src, 'mods')\n\n// Registration is the directory listing: every src/mods/*.tsx is a mod, in filename order.\n// index.ts is generated so config.tsx has a static import to typecheck and bundle.\nexport async function writeModsIndex() {\n\tawait mkdir(modsDir, { recursive: true })\n\tconst names = (await readdir(modsDir)).filter(name => /\\.tsx?$/.test(name) && name !== 'index.ts').map(name => name.replace(/\\.tsx?$/, '')).sort()\n\tconst contents = [\n\t\t'// Generated by build.mjs from the files in this directory. Do not edit.',\n\t\t\"import type { Mod } from '@/mod'\",\n\t\t...names.map((name, i) => `import * as m${i} from './${name}'`),\n\t\t`export const mods: Mod[] = [${names.map((_, i) => `m${i}`).join(', ')}]`,\n\t\t'',\n\t].join('\\n')\n\tconst target = join(modsDir, 'index.ts')\n\tif (await readFile(target, 'utf8').catch(() => null) !== contents) await writeFile(target, contents)\n\treturn names\n}\n\n// One document-script bundle. React, its JSX runtime, and tldraw stay external so the script binds to the app's own instances.\nexport const buildOptions = {\n\tentryPoints: [join(src, 'config.tsx')],\n\tbundle: true,\n\tformat: 'esm',\n\tplatform: 'browser',\n\tjsx: 'automatic',\n\talias: { '@': src },\n\texternal: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'tldraw'],\n\tplugins: [tailwindPlugin({ sources: [src] })],\n\tdefine: { 'process.env.NODE_ENV': '\"production\"' },\n\tloader: { '.png': 'dataurl' },\n}\n\nexport async function bundle() {\n\tawait writeModsIndex()\n\tconst result = await build({ ...buildOptions, write: false, logLevel: 'warning' })\n\treturn result.outputFiles[0].contents\n}\n\n// `node build.mjs`: bundle without an open document, as a syntax and import check.\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n\tconst names = await writeModsIndex()\n\tconst out = await bundle()\n\tconsole.log(`bundle ok: ${names.length} mods (${names.join(', ') || 'none'}) → ${out.length} bytes`)\n}\n",
      "type": "registry:file",
      "target": "~/build.mjs"
    },
    {
      "path": "workspace/apply.mjs",
      "content": "import { readFile, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { setTimeout } from 'node:timers/promises'\nimport { bundle } from './build.mjs'\n\n// Writes the bundle into each open document's script workspace through the desktop app's local API.\n// With no argument: every open local document inside this directory. With a path: that document, anywhere.\nconst root = dirname(fileURLToPath(import.meta.url))\nconst target = process.argv[2] ? resolve(process.argv[2]) : null\nconst appData = process.platform === 'darwin' ? join(homedir(), 'Library/Application Support')\n\t: process.platform === 'win32' ? process.env.APPDATA\n\t: process.env.XDG_CONFIG_HOME || join(homedir(), '.config')\nconst { port, token } = JSON.parse(await readFile(join(appData, 'tldraw/server.json'), 'utf8'))\n\nasync function request(path, body) {\n\tconst response = await fetch(`http://localhost:${port}${path}`, {\n\t\tmethod: body === undefined ? 'GET' : 'POST',\n\t\theaders: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },\n\t\tbody: body === undefined ? undefined : JSON.stringify(body),\n\t})\n\tconst data = await response.json()\n\tif (!response.ok || !data.success) throw new Error(JSON.stringify(data))\n\treturn data.result\n}\n\nconst docs = (await request('/api/search', { code: 'return await api.getDocs()' }))\n\t.filter(doc => doc.ownership === 'local' && (target ? doc.filePath === target : doc.filePath.startsWith(root + '/')))\nif (!docs.length) {\n\tconsole.error(target ? `Open ${target} in tldraw offline first.` : `No document from ${root} is open in tldraw offline.`)\n\tprocess.exit(2)\n}\nconst output = await bundle()\n\nasync function apply(doc) {\n\tconst base = `/api/doc/${doc.id}`\n\tconst workspace = await request(`${base}/script-workspace`, {})\n\tawait writeFile(join(workspace.scriptDir, 'config.js'), output)\n\tfor (let attempt = 0; attempt < 40; attempt++) {\n\t\tawait setTimeout(250)\n\t\tconst status = await request(`${base}/script-status`)\n\t\tif (status.state === 'error') throw new Error(JSON.stringify(status))\n\t\tif (status.state !== 'applied') continue\n\t\ttry {\n\t\t\tawait request(`${base}/exec`, { code: 'await helpers.saveDoc()' })\n\t\t} catch (error) {\n\t\t\t// The watcher can finish before config.js's new editor has mounted.\n\t\t\tif (error.message.includes('\"error\":\"Editor not mounted\"')) continue\n\t\t\tthrow error\n\t\t}\n\t\tconsole.log(`Applied and saved: ${doc.filePath}`)\n\t\treturn\n\t}\n\tthrow new Error(`${doc.filePath}: script watcher did not finish within 10 seconds. Inspect script-status before retrying.`)\n}\n\nconst results = await Promise.allSettled(docs.map(apply))\nfor (const result of results) if (result.status === 'rejected') console.error(result.reason.message)\nprocess.exit(results.some(result => result.status === 'rejected') ? 1 : 0)\n",
      "type": "registry:file",
      "target": "~/apply.mjs"
    },
    {
      "path": "workspace/tailwind-plugin.mjs",
      "content": "// esbuild plugin: every imported `.css` file is a Tailwind v4 stylesheet, compiled against the classes used under `src/`\n// and delivered to the script as a string (it is injected through a <style> element, never a stylesheet link).\nimport { compile } from '@tailwindcss/node'\nimport { Scanner } from '@tailwindcss/oxide'\nimport { readFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport function tailwindPlugin({ sources }) {\n\treturn {\n\t\tname: 'tailwind',\n\t\tsetup(build) {\n\t\t\tbuild.onLoad({ filter: /\\.css$/ }, async ({ path }) => {\n\t\t\t\tconst watchFiles = [path]\n\t\t\t\tconst compiler = await compile(await readFile(path, 'utf8'), { base: dirname(path), onDependency: file => watchFiles.push(file) })\n\t\t\t\tconst candidates = new Scanner({ sources: sources.map(base => ({ base, pattern: '**/*', negated: false })) }).scan()\n\t\t\t\treturn { contents: compiler.build(candidates), loader: 'text', watchFiles }\n\t\t\t})\n\t\t},\n\t}\n}\n",
      "type": "registry:file",
      "target": "~/tailwind-plugin.mjs"
    },
    {
      "path": "workspace/src/config.tsx",
      "content": "import type { ConfigScriptContext } from 'tldraw-offline/script-context'\nimport { DefaultContextMenu, DefaultContextMenuContent, DefaultToolbar, DefaultToolbarContent, TldrawUiMenuGroup, TldrawUiMenuItem, useEditor, useValue, type TLUiContextMenuProps } from 'tldraw'\nimport { mods } from './mods'\nimport { uiCss } from './ui'\n// Compiled by the build's Tailwind plugin from src/styles/globals.css against the classes used under src/.\nimport tailwindCss from './styles/globals.css'\n\nconst css = `${tailwindCss}\\n${uiCss}`\nconst tools = mods.flatMap(mod => mod.tool ? [mod.tool] : [])\nconst commands = mods.flatMap(mod => mod.commands ?? [])\n\n// Mod tools follow tldraw's stock toolbar items.\nfunction ModToolItem({ id, label, icon, kbd }: (typeof tools)[number]) {\n\tconst editor = useEditor()\n\tconst isSelected = useValue('current tool', () => editor.getCurrentToolId() === id, [editor, id])\n\treturn <TldrawUiMenuItem id={id} label={label} kbd={kbd} icon={icon} isSelected={isSelected} onSelect={() => { editor.setCurrentTool(id) }} />\n}\n\nfunction Toolbar() {\n\treturn (\n\t\t<>\n\t\t\t<style>{css}</style>\n\t\t\t<DefaultToolbar>\n\t\t\t\t<DefaultToolbarContent />\n\t\t\t\t{tools.map(tool => <ModToolItem key={tool.id} {...tool} />)}\n\t\t\t</DefaultToolbar>\n\t\t</>\n\t)\n}\n\nfunction ContextMenu(props: TLUiContextMenuProps) {\n\tconst editor = useEditor()\n\tconst readonly = useValue('readonly', () => editor.getIsReadonly(), [editor])\n\tconst visible = commands.filter(command => command.readonlyOk || !readonly)\n\treturn (\n\t\t<DefaultContextMenu {...props}>\n\t\t\t{visible.length > 0 && (\n\t\t\t\t<TldrawUiMenuGroup id=\"mods\">\n\t\t\t\t\t{visible.map(command => <TldrawUiMenuItem key={command.id} id={command.id} label={command.label} onSelect={() => command.run(editor)} />)}\n\t\t\t\t</TldrawUiMenuGroup>\n\t\t\t)}\n\t\t\t<DefaultContextMenuContent />\n\t\t</DefaultContextMenu>\n\t)\n}\n\nexport default async function (ctx: ConfigScriptContext) {\n\tfor (const mod of mods) {\n\t\tconst partial = await mod.default(ctx)\n\t\tif (partial) ctx.config = { ...ctx.config, ...partial }\n\t}\n\tctx.config.components = { ...ctx.config.components, Toolbar, ContextMenu }\n\treturn ctx.config\n}\n",
      "type": "registry:file",
      "target": "~/src/config.tsx"
    },
    {
      "path": "workspace/src/mod.ts",
      "content": "import type { Editor, TLUiIconJsx } from 'tldraw'\nimport type { BoardScriptConfig } from 'tldraw-offline/script-context'\n\n// A mod is a file in src/mods/ whose default export is the app's own config-script function:\n// it receives { config } before the editor mounts and pushes shape utils, tools, components, etc.\n// The build lists src/mods/ into src/mods/index.ts; config.tsx runs every mod in filename order.\nexport type ModConfig = BoardScriptConfig\n\n// Optional named exports the shell surfaces in the UI.\n// `tool`: a toolbar button that activates the tool with this id (one of the tools the mod pushed).\nexport interface ModTool { id: string; label: string; icon: TLUiIconJsx; kbd?: string }\n// `commands`: context-menu entries run against the editor.\nexport interface ModCommand { id: string; label: string; icon?: TLUiIconJsx; readonlyOk?: boolean; run(editor: Editor): void }\n\nexport interface Mod { default: ModConfig; tool?: ModTool; commands?: ModCommand[] }\n",
      "type": "registry:file",
      "target": "~/src/mod.ts"
    },
    {
      "path": "workspace/src/ui.ts",
      "content": "// The design system. One token set and one rule per surface recipe; each recipe's selector list is our `ui-*` hook\n// followed by every tldraw element that is that surface, so a look is defined in exactly one place.\n// Components use the hooks (`ui-panel`, `ui-well`, `ui-key`, `ui-kbd`, `ui-label`, `ui-option`, `ui-icon-button`,\n// `ui-rule-top`, `ui-rule-bottom`; `ui-round` makes a surface and the keys inside it pill-shaped) and never restate colors, faces, or shadows.\n// Depth is soft: a one-pixel highlight along the top of raised faces, a one-pixel shade along the bottom, no rims.\n\nconst tokens = `\n:where(.tl-container) {\n --ui-font:-apple-system,BlinkMacSystemFont,\"Inter\",\"Segoe UI\",Helvetica,Arial,sans-serif;\n --ui-mono:ui-monospace,\"SF Mono\",Menlo,Consolas,monospace;\n --ui-radius:10px; --ui-radius-key:7px; --ui-radius-kbd:4px;\n --ui-ink:#262626; --ui-ink-2:#737373; --ui-ring:#a3a3a3;\n --ui-bg:#ffffff; --ui-panel-solid:#fafafa; --ui-line:#0000000f;\n --ui-panel:linear-gradient(180deg,#fdfdfd,#f4f4f4);\n --ui-key:linear-gradient(180deg,#ffffff,#ededed);\n --ui-key-hover:linear-gradient(180deg,#ffffff,#f5f5f5);\n --ui-selected:linear-gradient(180deg,#f0f0f0,#e1e1e1);\n --ui-well:linear-gradient(180deg,#ededed,#f7f7f7);\n --ui-highlight:#ffffff; --ui-shade:#0000000d; --ui-shade-2:#0000001a; --ui-drop:#00000012;\n --ui-shadow-panel:inset 0 1px 0 var(--ui-highlight),0 1px 2px var(--ui-shade-2),0 12px 24px var(--ui-drop);\n --ui-shadow-attached:inset 0 1px 0 var(--ui-highlight),0 2px 6px var(--ui-drop);\n --ui-shadow-key:inset 0 1px 0 var(--ui-highlight),inset 0 -1px 0 var(--ui-shade),0 1px 2px var(--ui-shade-2);\n --ui-shadow-selected:inset 0 1px 0 var(--ui-highlight),inset 0 -1px 0 var(--ui-shade),0 1px 3px var(--ui-shade-2);\n --ui-shadow-well:inset 0 2px 4px var(--ui-shade-2),0 1px 0 var(--ui-highlight);\n}\n.tl-theme__dark {\n --ui-ink:#fafafa; --ui-ink-2:#a3a3a3; --ui-ring:#8a8a8a;\n --ui-bg:#0f0f0f; --ui-panel-solid:#1c1c1c; --ui-line:#ffffff12;\n --ui-panel:linear-gradient(180deg,#222222,#1a1a1a);\n --ui-key:linear-gradient(180deg,#343434,#282828);\n --ui-key-hover:linear-gradient(180deg,#3d3d3d,#2f2f2f);\n --ui-selected:linear-gradient(180deg,#4a4a4a,#3a3a3a);\n --ui-well:linear-gradient(180deg,#0f0f0f,#171717);\n --ui-highlight:#ffffff14; --ui-shade:#00000059; --ui-shade-2:#00000080; --ui-drop:#00000059;\n}\n.tl-container {\n --tl-color-panel:var(--ui-panel-solid); --tl-color-low:var(--ui-panel-solid); --tl-color-low-border:transparent;\n --tl-color-divider:var(--ui-line); --tl-color-hint:var(--ui-line); --tl-color-muted-2:var(--ui-line);\n --tl-color-text:var(--ui-ink); --tl-color-text-1:var(--ui-ink); --tl-color-text-3:var(--ui-ink-2);\n --tl-color-tooltip:var(--ui-panel-solid); --tl-color-text-shadow:var(--ui-ink); --tl-color-focus:var(--ui-ring);\n --tl-shadow-1:var(--ui-shadow-attached); --tl-shadow-2:var(--ui-shadow-panel); --tl-shadow-3:var(--ui-shadow-panel); --tl-shadow-4:var(--ui-shadow-panel);\n --tl-radius-2:6px; --tl-radius-3:var(--ui-radius); --tl-radius-4:var(--ui-radius);\n}\n.tl-theme__dark { --tl-color-background:var(--ui-bg); }\n@media (prefers-contrast:more) { :where(.tl-container) { --ui-line:var(--ui-ink-2); --ui-shade:var(--ui-ink-2); --ui-highlight:var(--ui-ink-2); } }\n`\n\n// Surface recipes: [our hook, ...tldraw elements that are this surface].\nconst panel = ['.ui-panel', '.tlui-main-toolbar__tools', '.tlui-style-panel__wrapper', '.tlui-menu', '.tlui-popover__content', '.tlui-hover-card__content', '.tlui-dialog__content', '.tlui-toast__container', '.tlui-cmt-thread', '.tlui-tooltip']\nconst attached = ['.ui-panel--attached', '.tlui-main-toolbar__extras__controls', '.tlui-menu-zone', '.tlui-navigation-panel::before', '.tlui-help-menu .tlui-button__low']\nconst key = ['.ui-key', '.ui-kbd', '.tlui-kbd > span', '.tlui-slider__thumb', '.tlui-button__low', '.tlui-button__menu[data-highlighted]::after', '.tlui-dialog__footer__actions .tlui-button::after']\nconst keyHover = ['.ui-key:where(:hover:not(:disabled,[aria-pressed=true]))', '.tlui-button:not(:disabled,[data-disabled]):hover::after']\nconst selected = ['.ui-key[aria-pressed=true]', '.ui-option[aria-selected=true]', '.tlui-button[data-isactive=true]::after', '.tlui-button[data-isactive=true]:not(:disabled,[data-disabled],:focus-visible):active::after', '.tlui-button__tool[aria-pressed=true]:not(:disabled,[data-disabled])::after', '.tlui-page-menu__item[data-iscurrent=true]:not([data-editing=true])::before', '.tlui-slider__range']\nconst well = ['.ui-well', '.ui-key:where(:active:not(:disabled))', '.tlui-button:not(:disabled,[data-disabled]):active::after', '.tlui-button[data-state=open]::after', '.tlui-zoom-menu__button[data-state=open]::after', '.tlui-page-menu__trigger[data-state=open]::after', '.tlui-menu-zone [data-state=open]::after', '.tlui-input__wrapper .tlui-input', '.tlui-page-menu__item__input', '.tlui-slider__track::after', '.tlui-slider__thumb:active']\nconst ruleTop = ['.ui-rule-top', '.tlui-dialog__footer']\nconst ruleBottom = ['.ui-rule-bottom', '.tlui-dialog__header', '.tlui-menu__group:not(:last-of-type)', '.tlui-style-panel__section:not(:nth-last-child(-n+1 of .tlui-style-panel__section:not(:empty)))']\n\nconst recipes = `\n${panel} { background:var(--ui-panel); box-shadow:var(--ui-shadow-panel); border-radius:var(--ui-radius); color:var(--ui-ink); }\n${attached} { background:var(--ui-panel); box-shadow:var(--ui-shadow-attached); border-color:transparent; }\n${key} { background:var(--ui-key); box-shadow:var(--ui-shadow-key); color:var(--ui-ink); border-radius:var(--ui-radius-key); opacity:1; }\n@media (hover:hover) { ${keyHover} { background:var(--ui-key-hover); box-shadow:var(--ui-shadow-key); opacity:1; } }\n${selected} { background:var(--ui-selected); box-shadow:var(--ui-shadow-selected); opacity:1; }\n${well} { background:var(--ui-well); box-shadow:var(--ui-shadow-well); opacity:1; }\n${ruleTop} { box-shadow:inset 0 1px 0 var(--ui-line),inset 0 2px 0 var(--ui-highlight); }\n${ruleBottom} { box-shadow:inset 0 -1px 0 var(--ui-line); border-bottom:0; }\n\n/* Hooks with their own geometry. */\n.ui-round,.ui-round .ui-key { border-radius:999px; }\n.ui-key { border:0; cursor:pointer; transition:transform 120ms ease,background 90ms ease,box-shadow 90ms ease; }\n.ui-key:where(:active:not(:disabled)) { transform:translateY(1px); }\n.ui-key:disabled { opacity:.35; cursor:default; }\n.ui-icon-button { display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; flex-shrink:0; padding:0; border:0; background:transparent; color:var(--ui-ink-2); cursor:pointer; transition:color 120ms ease,transform 120ms ease; }\n.ui-icon-button:where(:hover:not(:disabled)),.ui-icon-button[aria-pressed=true] { color:var(--ui-ink); }\n.ui-icon-button:disabled { opacity:.35; cursor:default; }\n.ui-icon-button svg { width:16px; height:16px; }\n.ui-kbd,.tlui-kbd > span { display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:var(--ui-radius-kbd); font:10px/1 var(--ui-mono); font-variant-numeric:tabular-nums; letter-spacing:.02em; }\n.tlui-kbd { display:inline-flex; gap:3px; font-family:var(--ui-mono); }\n.tlui-kbd > span:last-child { padding-inline-end:5px; }\n.ui-label { color:var(--ui-ink-2); font:10px/1 var(--ui-mono); letter-spacing:.1em; text-transform:uppercase; }\n.ui-option { display:flex; align-items:center; gap:10px; height:36px; padding:0 10px; border-radius:var(--ui-radius-key); color:var(--ui-ink); cursor:pointer; }\n.ui-option[aria-selected=true] { color:var(--ui-ink); }\n:is(.ui-key,.ui-icon-button,.ui-option):focus-visible { outline:2px solid var(--ui-ring); outline-offset:-2px; }\n.ui-well:has(input,textarea) :is(input,textarea):focus-visible { outline:none; box-shadow:none; }\n@media (prefers-reduced-motion:reduce) { .ui-key,.ui-icon-button { transition:none; } .ui-key:where(:active:not(:disabled)) { transform:none; } }\n`\n\n// tldraw geometry that its stylesheet fixes and the recipes need loosened.\nconst tldraw = `\n.tlui-layout,.tlui-menu,.tlui-popover__content,.tlui-dialog__content,.tlui-tooltip,.tlui-toast__container,.tlui-hover-card__content { font-family:var(--ui-font); font-weight:450; letter-spacing:.005em; }\n.tlui-main-toolbar__extras__controls { border-radius:var(--ui-radius) var(--ui-radius) 0 0; }\n.tlui-help-menu { border-color:transparent; }\n.tlui-button::after { border-radius:var(--tl-radius-2); transition:opacity 90ms ease,background 90ms ease,box-shadow 90ms ease; }\n.tlui-button__tool::after { inset:4px; border-radius:8px; }\n.tlui-button__low::after { content:none; }\n.tlui-button[data-isactive=true],.tlui-button__tool[aria-pressed=true],.tlui-button__primary { color:var(--ui-ink); }\n.tlui-tooltip { padding:4px 8px; border-radius:var(--tl-radius-2); color:var(--ui-ink); }\n.tlui-tooltip__arrow { fill:var(--ui-panel-solid); }\n.tlui-slider__track::after,.tlui-slider__range { height:6px; top:calc(50% - 3px); border-radius:3px; }\n.tlui-slider__thumb { width:18px; height:18px; border-radius:50%; }\n.tlui-input__wrapper .tlui-input,.tlui-page-menu__item__input { border-radius:var(--tl-radius-2); padding:0 8px; }\n.tlui-page-menu__item__title::before { border-color:var(--ui-ring); }\n.tlui-dialog__header__title { font-weight:550; }\n.tlui-dialog__footer__actions .tlui-button::after { inset:6px 4px; }\n.tlui-dialog__footer__actions .tlui-button__primary::after { background:var(--ui-ink); box-shadow:inset 0 1px 0 #ffffff59; }\n.tlui-dialog__footer__actions .tlui-button__primary { color:var(--ui-panel-solid); }\n.desktop-share__trigger.tlui-button::before { background:var(--ui-ink); border-radius:var(--tl-radius-2); box-shadow:inset 0 1px 0 #ffffff59,inset 0 -1px 0 #00000026; }\n.desktop-share__trigger.tlui-button::after { background:var(--ui-panel); box-shadow:var(--ui-shadow-attached); }\n.desktop-share__trigger.tlui-button:not(:disabled,[data-disabled]):hover::before,.desktop-share__trigger.tlui-button[data-state=open]::before { background:var(--ui-ink-2); }\n.desktop-share__trigger.tlui-button { color:var(--ui-panel-solid); }\n.tl-container__focused:not(.tl-container__no-focus-ring) .tlui-button:focus-visible { outline:2px solid var(--ui-ring); outline-offset:-2px; border-radius:var(--tl-radius-2); }\n@media (prefers-reduced-motion:reduce) { .tlui-button::after { transition:none; } }\n`\n\nexport const uiCss = tokens + recipes + tldraw\n",
      "type": "registry:file",
      "target": "~/src/ui.ts"
    },
    {
      "path": "workspace/src/assets.d.ts",
      "content": "// Mirrors the esbuild loaders in build.mjs: images arrive as data URLs, stylesheets as text.\ndeclare module '*.png' { const dataUrl: string; export default dataUrl }\ndeclare module '*.css' { const text: string; export default text }\n",
      "type": "registry:file",
      "target": "~/src/assets.d.ts"
    },
    {
      "path": "workspace/src/styles/globals.css",
      "content": "/*\n * shadcn's global stylesheet, wired to the design language in ui.ts. Every shadcn token (--background, --primary, …)\n * is derived from a --ui-* token, so ui.ts stays the single source of truth and light/dark flip through it: there is\n * no separate .dark block. Tailwind additionally exposes the language's surfaces as utilities:\n *   bg-panel / bg-key / bg-key-hover / bg-selected / bg-well   (the gradient faces)\n *   shadow-panel / shadow-attached / shadow-key / shadow-selected / shadow-well   (the soft depth)\n *   rounded-sm|md|lg = kbd 4px | key 7px | panel 10px;  font-sans / font-mono = --ui-font / --ui-mono\n *\n * Differences from a stock shadcn app, forced by living inside the tldraw app:\n * 1. No Preflight: theme + utilities only. Preflight would reset tldraw's own UI. Utilities are deliberately NOT in a\n *    cascade layer: the app's own (unlayered) stylesheet would beat any layered rule regardless of specificity, so\n *    `text-foreground` would lose to tldraw's `button { color: inherit }`. Unlayered, they win on class specificity.\n * 2. Tokens are declared on .tl-container (where ui.ts declares --ui-*), never :root, and `dark:` keys off tldraw's\n *    .tl-theme__dark. Radii are also declared as real custom properties there because components reference\n *    var(--radius-md) inside arbitrary values, which need a property that resolves in the tree.\n * 3. No `@layer base` body/universal rules: they would restyle the whole app, not our components.\n */\n@import \"tailwindcss/theme.css\" layer(theme);\n@import \"tailwindcss/utilities.css\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n\n@custom-variant dark (&:is(.tl-theme__dark *));\n\n@theme inline {\n  --font-sans: var(--ui-font);\n  --font-mono: var(--ui-mono);\n\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n\n  --radius-sm: var(--radius-sm);\n  --radius-md: var(--radius-md);\n  --radius-lg: var(--radius-lg);\n  --radius-xl: var(--radius-xl);\n  --radius-2xl: var(--radius-2xl);\n  --radius-3xl: var(--radius-3xl);\n  --radius-4xl: var(--radius-4xl);\n\n  --background-image-panel: var(--ui-panel);\n  --background-image-key: var(--ui-key);\n  --background-image-key-hover: var(--ui-key-hover);\n  --background-image-selected: var(--ui-selected);\n  --background-image-well: var(--ui-well);\n\n  --shadow-panel: var(--ui-shadow-panel);\n  --shadow-attached: var(--ui-shadow-attached);\n  --shadow-key: var(--ui-shadow-key);\n  --shadow-selected: var(--ui-shadow-selected);\n  --shadow-well: var(--ui-shadow-well);\n\n  /* A light sweep across a surface that is being regenerated. */\n  --animate-shimmer: shimmer 1800ms ease-in-out infinite;\n  @keyframes shimmer {\n    from { transform: translateX(-100%); }\n    to { transform: translateX(100%); }\n  }\n}\n\n.tl-container {\n  --radius-sm: var(--ui-radius-kbd);\n  --radius-md: var(--ui-radius-key);\n  --radius-lg: var(--ui-radius);\n  --radius-xl: calc(var(--ui-radius) * 1.4);\n  --radius-2xl: calc(var(--ui-radius) * 1.8);\n  --radius-3xl: calc(var(--ui-radius) * 2.2);\n  --radius-4xl: calc(var(--ui-radius) * 2.6);\n\n  --background: var(--ui-bg);\n  --foreground: var(--ui-ink);\n  --card: var(--ui-panel-solid);\n  --card-foreground: var(--ui-ink);\n  --popover: var(--ui-panel-solid);\n  --popover-foreground: var(--ui-ink);\n  /* Primary is ink on paper: the strongest thing in a zero-chroma language. */\n  --primary: var(--ui-ink);\n  --primary-foreground: var(--ui-bg);\n  /* Secondary, muted, and accent are the same quiet face; the language has one neutral step, not three. */\n  --secondary: color-mix(in srgb, var(--ui-ink) 6%, var(--ui-bg));\n  --secondary-foreground: var(--ui-ink);\n  --muted: color-mix(in srgb, var(--ui-ink) 6%, var(--ui-bg));\n  --muted-foreground: var(--ui-ink-2);\n  --accent: color-mix(in srgb, var(--ui-ink) 6%, var(--ui-bg));\n  --accent-foreground: var(--ui-ink);\n  --border: var(--ui-line);\n  --input: var(--ui-line);\n  --ring: var(--ui-ring);\n  /* The one chromatic token; shadcn's own values, since the language has no red. */\n  --destructive: oklch(0.577 0.245 27.325);\n  --destructive-foreground: var(--ui-bg);\n}\n\n.tl-theme__dark {\n  --destructive: oklch(0.704 0.191 22.216);\n}\n",
      "type": "registry:file",
      "target": "~/src/styles/globals.css"
    },
    {
      "path": "workspace/src/lib/utils.ts",
      "content": "export { cn } from 'cn'\n",
      "type": "registry:file",
      "target": "~/src/lib/utils.ts"
    }
  ],
  "docs": "Build: npm run build. Apply to the open document: npm run apply [path.tldraw]. Add mods with: npx tldraw-mods add <name>.",
  "categories": [
    "workspace"
  ],
  "type": "registry:item"
}