{"schemaVersion":1,"id":"recommended.sm-video-assets","version":"1.0.0","name":"Video Assets workflow","description":"An editable production workflow using Video Assets v2.1.0.","stages":[{"id":"canvas","blockId":"sm.pipeline","version":"2.2.2","label":"Canvas"},{"id":"main","blockId":"sm.video-assets","version":"2.1.0","label":"Video Assets"},{"id":"edit","blockId":"sm.editor","version":"2.2.7","label":"Editor"}],"connections":[],"packages":[{"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"sm.video-assets","version":"2.1.0","name":"Video Assets","description":"Review every stored media take, choose exact asset versions, and pass them to the next production step in a deliberate order.","kind":"workspace","runtime":"javascript","entry":"src/run.js","license":"MIT","provenance":{"notice":"Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js, src/view.json\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","portableSources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}]},"inputs":{"assets":{"type":"asset[]","context":"assets","primary":true,"required":false,"default":[],"description":"Existing project media, retaining asset and version identities."},"kind":{"type":"text","default":"All","description":"Limit the workspace to one media kind."},"selectedIds":{"type":"text","default":"","description":"Newline-separated asset IDs selected in this workspace; blank keeps every matching asset."},"selectedVersions":{"type":"text","default":"","description":"Newline-separated assetId::versionId pairs in output order. This takes precedence over legacy asset-only selection."}},"outputs":{"output":{"type":"asset[]","primary":true,"role":"production_media","description":"Selected original media references, ready for another Block or the Editor."}},"permissions":{"project":["context.assets.read","asset.read"],"network":[],"filesystem":[],"secrets":[]},"ui":[{"port":"kind","control":"select","options":["All","Images","Videos","Audio"]},{"port":"selectedIds","control":"text"},{"port":"selectedVersions","control":"text"}]},"code":"const assets = input.assets || [];\nif (assets.length > 500) throw new Error('Video Assets accepts up to 500 project media versions at once.');\nconst kinds = {All: '', Images: 'image', Videos: 'video', Audio: 'audio'};\nif (!Object.hasOwn(kinds, input.kind)) throw new Error('Choose All, Images, Videos, or Audio.');\nconst available = assets.filter(asset => !kinds[input.kind] || asset.kind === kinds[input.kind]);\nconst keys = input.selectedVersions.split(/\\r?\\n/).map(value => value.trim()).filter(Boolean);\nif (keys.length > 500 || new Set(keys).size !== keys.length) throw new Error('Choose up to 500 distinct asset versions.');\nif (keys.length) {\n const selected = keys.map(key => {\n  const split = key.indexOf('::');\n  if (split < 1 || split === key.length - 2) throw new Error('Choose versions as assetId::versionId, one per line.');\n  const assetId = key.slice(0, split), versionId = key.slice(split + 2);\n  return available.find(asset => asset.assetId === assetId && asset.versionId === versionId);\n });\n if (selected.some(asset => !asset)) throw new Error('One or more selected media versions are unavailable in this project or filter.');\n return {output: selected};\n}\nconst ids = input.selectedIds.split(/\\r?\\n/).map(value => value.trim()).filter(Boolean);\nif (ids.length > 500 || new Set(ids).size !== ids.length) throw new Error('Choose up to 500 distinct asset IDs.');\nif (!ids.length) return {output: available};\nconst selected = available.filter(asset => ids.includes(asset.assetId));\nif (new Set(selected.map(asset => asset.assetId)).size !== ids.length) throw new Error('One or more selected assets are unavailable in this project or filter.');\nreturn {output: selected};\n","view":{"html":"<main class=\"assets-workspace\">\n  <header><h1>Video Assets</h1><p>Choose the original media versions you want to send forward.</p></header>\n  <div class=\"toolbar\">\n    <label for=\"kind\">Show</label>\n    <select id=\"kind\"><option>All</option><option>Images</option><option>Videos</option><option>Audio</option></select>\n    <button id=\"all\" type=\"button\">Select all</button>\n    <button id=\"none\" type=\"button\">Clear</button>\n  </div>\n  <section id=\"assets\" class=\"asset-list\" aria-label=\"Project media\"><p>No project media is available yet.</p></section>\n  <section class=\"selection-panel\" aria-label=\"Selected media order\"><h2>Selected order</h2><p>Move exact takes into the order the next Block should receive them.</p><div id=\"selection\" class=\"selection-list\"></div></section>\n  <div class=\"footer\"><button id=\"use\" data-stillmade-action=\"run\" type=\"button\">Use selected versions</button><output id=\"status\" role=\"status\" aria-live=\"polite\">Waiting for project media.</output></div>\n</main>\n","css":".assets-workspace { box-sizing: border-box; max-width: 960px; min-height: 100%; margin: 0 auto; padding: 24px; color: var(--fg); background: var(--bg); font-family: var(--font-body); }\n.assets-workspace h1 { margin: 0 0 8px; font-family: var(--font-display); font-size: 36px; font-style: italic; font-weight: 200; }\n.assets-workspace header p, .assets-workspace output { color: var(--fg-muted); }\n.toolbar, .footer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 20px 0; }\n.toolbar select, .assets-workspace button { min-height: 44px; padding: 9px 14px; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-elev-1); color: var(--fg); font: inherit; }\n.assets-workspace button { cursor: pointer; }\n.assets-workspace button:disabled { opacity: .5; cursor: default; }\n.asset-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }\n.asset { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start; min-width: 0; padding: 14px; border: 1px solid var(--border-tok); border-radius: 12px; background: var(--bg-elev-1); }\n.asset span { min-width: 0; overflow-wrap: anywhere; }\n.asset small { display: block; margin-top: 4px; color: var(--fg-subtle); }\n.selection-panel { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--divider); }\n.selection-panel h2 { margin: 0 0 5px; font-size: 18px; }\n.selection-panel > p { margin: 0 0 12px; color: var(--fg-muted); }\n.selection-list { display: grid; gap: 8px; }\n.selected { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 8px; align-items: center; padding: 10px 12px; border: 1px solid var(--border-tok); border-radius: 12px; background: var(--bg-elev-1); }\n.selected span { min-width: 0; overflow-wrap: anywhere; }\n.selected small { display: block; margin-top: 3px; color: var(--fg-subtle); }\n.selected button { min-width: 44px; padding: 8px 10px; }\n.footer #use { background: var(--fg); color: var(--bg); }\n.assets-workspace button:focus-visible, .assets-workspace select:focus-visible, .asset input:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }\n@media (max-width: 640px) { .assets-workspace { padding: 16px; } .asset-list { grid-template-columns: minmax(0, 1fr); } .selected { grid-template-columns: minmax(0, 1fr) auto auto; } .selected button:last-child { grid-column: 1 / -1; } }\n","javascript":"const kind = document.getElementById('kind');\nconst all = document.getElementById('all');\nconst none = document.getElementById('none');\nconst use = document.getElementById('use');\nconst status = document.getElementById('status');\nlet current = {};\nlet assets = [];\nlet selectedKeys = [];\nlet busy = false;\nlet revision = 0;\n\nconst keyOf = asset => asset.assetId + '::' + asset.versionId;\nconst mediaKind = value => value === 'Images' ? 'image' : value === 'Videos' ? 'video' : value === 'Audio' ? 'audio' : '';\nconst visible = () => assets.filter(asset => !mediaKind(kind.value) || asset.kind === mediaKind(kind.value));\nconst titleOf = asset => asset.name || asset.label || asset.assetId;\nconst detailOf = asset => (asset.kind || 'asset') + ' · version ' + asset.versionId + (asset.sceneNumber ? ' · scene ' + String(asset.sceneNumber) : '') + (asset.shotId ? ' · shot ' + String(asset.shotId) : '');\nconst draw = () => {\n  const shown = visible();\n  selectedKeys = selectedKeys.filter(key => shown.some(asset => keyOf(asset) === key));\n  StillMade.render('assets', shown.length ? shown.map((asset, index) => ({\n    tag: 'label', className: 'asset', children: [\n      {tag: 'input', id: 'asset-' + index, type: 'checkbox', action: 'asset:' + index, checked: selectedKeys.includes(keyOf(asset)), disabled: busy, label: titleOf(asset)},\n      {tag: 'span', children: [{tag: 'strong', text: titleOf(asset)}, {tag: 'small', text: detailOf(asset)}]}\n    ]\n  })) : [{tag: 'p', text: 'No matching project media is available.'}]);\n  StillMade.render('selection', selectedKeys.length ? selectedKeys.map((key, index) => {\n    const asset = shown.find(item => keyOf(item) === key);\n    return {tag: 'article', className: 'selected', children: [\n      {tag: 'span', children: [{tag: 'strong', text: String(index + 1) + '. ' + titleOf(asset)}, {tag: 'small', text: detailOf(asset)}]},\n      {tag: 'button', action: 'up:' + index, text: '↑', disabled: busy || index === 0, label: 'Move earlier'},\n      {tag: 'button', action: 'down:' + index, text: '↓', disabled: busy || index === selectedKeys.length - 1, label: 'Move later'},\n      {tag: 'button', action: 'remove:' + index, text: 'Remove', disabled: busy}\n    ]};\n  }) : [{tag: 'p', text: 'No versions selected.'}]);\n  all.disabled = busy || !shown.length;\n  none.disabled = busy || !selectedKeys.length;\n  use.disabled = busy || !selectedKeys.length;\n  status.textContent = selectedKeys.length ? String(selectedKeys.length) + ' exact version' + (selectedKeys.length === 1 ? '' : 's') + ' selected in output order.' : shown.length ? 'Select media to continue.' : 'No matching media.';\n};\n\nStillMade.onInput(values => {\n  current = values;\n  revision += 1;\n  assets = (values.assets || []).slice(0, 500);\n  kind.value = values.kind || 'All';\n  const exact = (values.selectedVersions || '').split(/\\r?\\n/).map(value => value.trim()).filter(key => assets.some(asset => keyOf(asset) === key));\n  const legacy = (values.selectedIds || '').split(/\\r?\\n/).map(value => value.trim()).filter(Boolean);\n  selectedKeys = exact.length ? exact : legacy.length ? assets.filter(asset => legacy.includes(asset.assetId)).map(keyOf) : visible().map(keyOf);\n  draw();\n});\nStillMade.onAction(event => {\n  if (busy) return;\n  const parts = event.action.split(':');\n  const action = parts[0], index = Number(parts[1]);\n  if (action === 'asset') {\n    const asset = visible().find((item, position) => position === index);\n    if (!asset) return;\n    const key = keyOf(asset);\n    selectedKeys = event.checked ? [...selectedKeys.filter(value => value !== key), key] : selectedKeys.filter(value => value !== key);\n  }\n  if (action === 'remove') selectedKeys = selectedKeys.filter((value, position) => position !== index);\n  if (action === 'up' && index > 0) { const item = selectedKeys.find((value, position) => position === index), before = selectedKeys.find((value, position) => position === index - 1); selectedKeys = selectedKeys.map((value, position) => position === index - 1 ? item : position === index ? before : value); }\n  if (action === 'down' && index < selectedKeys.length - 1) { const item = selectedKeys.find((value, position) => position === index), after = selectedKeys.find((value, position) => position === index + 1); selectedKeys = selectedKeys.map((value, position) => position === index ? after : position === index + 1 ? item : value); }\n  draw();\n});\nkind.addEventListener('change', () => { if (!busy) { selectedKeys = visible().map(keyOf); draw(); } });\nall.addEventListener('click', () => { if (!busy) { selectedKeys = visible().map(keyOf); draw(); } });\nnone.addEventListener('click', () => { if (!busy) { selectedKeys = []; draw(); } });\nuse.addEventListener('click', async () => {\n  if (busy || !selectedKeys.length) return;\n  const started = revision;\n  busy = true;\n  draw();\n  let message;\n  try {\n    const result = await StillMade.run({assets: current.assets || [], kind: kind.value, selectedIds: '', selectedVersions: selectedKeys.join('\\n')});\n    message = String((result.output || []).length) + ' exact media versions are ready for the next step.';\n  } catch (error) { message = error.message || 'The selection could not be prepared.'; }\n  finally { busy = false; draw(); if (started === revision) status.textContent = message; }\n});\n"},"tests":[{"name":"Keep every project media item by default","input":{"assets":[{"kind":"image","assetId":"image-1","versionId":"image-v1","url":"https://media.example/image.png","name":"Opening"},{"kind":"video","assetId":"video-1","versionId":"video-v2","url":"https://media.example/video.mp4","name":"Action"}],"kind":"All","selectedIds":""},"expected":{"output":[{"kind":"image","assetId":"image-1","versionId":"image-v1","url":"https://media.example/image.png","name":"Opening"},{"kind":"video","assetId":"video-1","versionId":"video-v2","url":"https://media.example/video.mp4","name":"Action"}]}},{"name":"Choose one original video version","input":{"assets":[{"kind":"image","assetId":"image-1","versionId":"image-v1","url":"https://media.example/image.png"},{"kind":"video","assetId":"video-1","versionId":"video-v2","url":"https://media.example/video.mp4"}],"kind":"Videos","selectedIds":"video-1"},"expected":{"output":[{"kind":"video","assetId":"video-1","versionId":"video-v2","url":"https://media.example/video.mp4"}]}},{"name":"Choose exact takes in editor order","input":{"assets":[{"kind":"video","assetId":"shot-1","versionId":"take-1","url":"https://media.example/shot-1-take-1.mp4","name":"Opening","sceneNumber":1,"shotId":"1a"},{"kind":"video","assetId":"shot-1","versionId":"take-2","url":"https://media.example/shot-1-take-2.mp4","name":"Opening","sceneNumber":1,"shotId":"1a"},{"kind":"video","assetId":"shot-2","versionId":"take-1","url":"https://media.example/shot-2-take-1.mp4","name":"Ending","sceneNumber":2,"shotId":"2a"},{"kind":"image","assetId":"frame-1","versionId":"still-1","url":"https://media.example/frame.png","name":"Still"}],"kind":"Videos","selectedIds":"","selectedVersions":"shot-2::take-1\nshot-1::take-2"},"expected":{"output":[{"kind":"video","assetId":"shot-2","versionId":"take-1","url":"https://media.example/shot-2-take-1.mp4","name":"Ending","sceneNumber":2,"shotId":"2a"},{"kind":"video","assetId":"shot-1","versionId":"take-2","url":"https://media.example/shot-1-take-2.mp4","name":"Opening","sceneNumber":1,"shotId":"1a"}]}}],"portable":{"format":"stillmade-block/1","dependencies":[],"sources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}],"files":{"upstream/0/LICENSE":"MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","upstream/0/src/run.js":"const assets = input.assets || [];\nif (assets.length > 500) throw new Error('Video Assets accepts up to 500 project media versions at once.');\nconst kinds = {All: '', Images: 'image', Videos: 'video', Audio: 'audio'};\nif (!Object.hasOwn(kinds, input.kind)) throw new Error('Choose All, Images, Videos, or Audio.');\nconst available = assets.filter(asset => !kinds[input.kind] || asset.kind === kinds[input.kind]);\nconst keys = input.selectedVersions.split(/\\r?\\n/).map(value => value.trim()).filter(Boolean);\nif (keys.length > 500 || new Set(keys).size !== keys.length) throw new Error('Choose up to 500 distinct asset versions.');\nif (keys.length) {\n const selected = keys.map(key => {\n  const split = key.indexOf('::');\n  if (split < 1 || split === key.length - 2) throw new Error('Choose versions as assetId::versionId, one per line.');\n  const assetId = key.slice(0, split), versionId = key.slice(split + 2);\n  return available.find(asset => asset.assetId === assetId && asset.versionId === versionId);\n });\n if (selected.some(asset => !asset)) throw new Error('One or more selected media versions are unavailable in this project or filter.');\n return {output: selected};\n}\nconst ids = input.selectedIds.split(/\\r?\\n/).map(value => value.trim()).filter(Boolean);\nif (ids.length > 500 || new Set(ids).size !== ids.length) throw new Error('Choose up to 500 distinct asset IDs.');\nif (!ids.length) return {output: available};\nconst selected = available.filter(asset => ids.includes(asset.assetId));\nif (new Set(selected.map(asset => asset.assetId)).size !== ids.length) throw new Error('One or more selected assets are unavailable in this project or filter.');\nreturn {output: selected};\n","upstream/0/src/view.json":"{\n  \"html\": \"<main class=\\\"assets-workspace\\\">\\n  <header><h1>Video Assets</h1><p>Choose the original media versions you want to send forward.</p></header>\\n  <div class=\\\"toolbar\\\">\\n    <label for=\\\"kind\\\">Show</label>\\n    <select id=\\\"kind\\\"><option>All</option><option>Images</option><option>Videos</option><option>Audio</option></select>\\n    <button id=\\\"all\\\" type=\\\"button\\\">Select all</button>\\n    <button id=\\\"none\\\" type=\\\"button\\\">Clear</button>\\n  </div>\\n  <section id=\\\"assets\\\" class=\\\"asset-list\\\" aria-label=\\\"Project media\\\"><p>No project media is available yet.</p></section>\\n  <section class=\\\"selection-panel\\\" aria-label=\\\"Selected media order\\\"><h2>Selected order</h2><p>Move exact takes into the order the next Block should receive them.</p><div id=\\\"selection\\\" class=\\\"selection-list\\\"></div></section>\\n  <div class=\\\"footer\\\"><button id=\\\"use\\\" data-stillmade-action=\\\"run\\\" type=\\\"button\\\">Use selected versions</button><output id=\\\"status\\\" role=\\\"status\\\" aria-live=\\\"polite\\\">Waiting for project media.</output></div>\\n</main>\\n\",\n  \"css\": \".assets-workspace { box-sizing: border-box; max-width: 960px; min-height: 100%; margin: 0 auto; padding: 24px; color: var(--fg); background: var(--bg); font-family: var(--font-body); }\\n.assets-workspace h1 { margin: 0 0 8px; font-family: var(--font-display); font-size: 36px; font-style: italic; font-weight: 200; }\\n.assets-workspace header p, .assets-workspace output { color: var(--fg-muted); }\\n.toolbar, .footer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 20px 0; }\\n.toolbar select, .assets-workspace button { min-height: 44px; padding: 9px 14px; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-elev-1); color: var(--fg); font: inherit; }\\n.assets-workspace button { cursor: pointer; }\\n.assets-workspace button:disabled { opacity: .5; cursor: default; }\\n.asset-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }\\n.asset { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start; min-width: 0; padding: 14px; border: 1px solid var(--border-tok); border-radius: 12px; background: var(--bg-elev-1); }\\n.asset span { min-width: 0; overflow-wrap: anywhere; }\\n.asset small { display: block; margin-top: 4px; color: var(--fg-subtle); }\\n.selection-panel { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--divider); }\\n.selection-panel h2 { margin: 0 0 5px; font-size: 18px; }\\n.selection-panel > p { margin: 0 0 12px; color: var(--fg-muted); }\\n.selection-list { display: grid; gap: 8px; }\\n.selected { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 8px; align-items: center; padding: 10px 12px; border: 1px solid var(--border-tok); border-radius: 12px; background: var(--bg-elev-1); }\\n.selected span { min-width: 0; overflow-wrap: anywhere; }\\n.selected small { display: block; margin-top: 3px; color: var(--fg-subtle); }\\n.selected button { min-width: 44px; padding: 8px 10px; }\\n.footer #use { background: var(--fg); color: var(--bg); }\\n.assets-workspace button:focus-visible, .assets-workspace select:focus-visible, .asset input:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }\\n@media (max-width: 640px) { .assets-workspace { padding: 16px; } .asset-list { grid-template-columns: minmax(0, 1fr); } .selected { grid-template-columns: minmax(0, 1fr) auto auto; } .selected button:last-child { grid-column: 1 / -1; } }\\n\",\n  \"javascript\": \"const kind = document.getElementById('kind');\\nconst all = document.getElementById('all');\\nconst none = document.getElementById('none');\\nconst use = document.getElementById('use');\\nconst status = document.getElementById('status');\\nlet current = {};\\nlet assets = [];\\nlet selectedKeys = [];\\nlet busy = false;\\nlet revision = 0;\\n\\nconst keyOf = asset => asset.assetId + '::' + asset.versionId;\\nconst mediaKind = value => value === 'Images' ? 'image' : value === 'Videos' ? 'video' : value === 'Audio' ? 'audio' : '';\\nconst visible = () => assets.filter(asset => !mediaKind(kind.value) || asset.kind === mediaKind(kind.value));\\nconst titleOf = asset => asset.name || asset.label || asset.assetId;\\nconst detailOf = asset => (asset.kind || 'asset') + ' · version ' + asset.versionId + (asset.sceneNumber ? ' · scene ' + String(asset.sceneNumber) : '') + (asset.shotId ? ' · shot ' + String(asset.shotId) : '');\\nconst draw = () => {\\n  const shown = visible();\\n  selectedKeys = selectedKeys.filter(key => shown.some(asset => keyOf(asset) === key));\\n  StillMade.render('assets', shown.length ? shown.map((asset, index) => ({\\n    tag: 'label', className: 'asset', children: [\\n      {tag: 'input', id: 'asset-' + index, type: 'checkbox', action: 'asset:' + index, checked: selectedKeys.includes(keyOf(asset)), disabled: busy, label: titleOf(asset)},\\n      {tag: 'span', children: [{tag: 'strong', text: titleOf(asset)}, {tag: 'small', text: detailOf(asset)}]}\\n    ]\\n  })) : [{tag: 'p', text: 'No matching project media is available.'}]);\\n  StillMade.render('selection', selectedKeys.length ? selectedKeys.map((key, index) => {\\n    const asset = shown.find(item => keyOf(item) === key);\\n    return {tag: 'article', className: 'selected', children: [\\n      {tag: 'span', children: [{tag: 'strong', text: String(index + 1) + '. ' + titleOf(asset)}, {tag: 'small', text: detailOf(asset)}]},\\n      {tag: 'button', action: 'up:' + index, text: '↑', disabled: busy || index === 0, label: 'Move earlier'},\\n      {tag: 'button', action: 'down:' + index, text: '↓', disabled: busy || index === selectedKeys.length - 1, label: 'Move later'},\\n      {tag: 'button', action: 'remove:' + index, text: 'Remove', disabled: busy}\\n    ]};\\n  }) : [{tag: 'p', text: 'No versions selected.'}]);\\n  all.disabled = busy || !shown.length;\\n  none.disabled = busy || !selectedKeys.length;\\n  use.disabled = busy || !selectedKeys.length;\\n  status.textContent = selectedKeys.length ? String(selectedKeys.length) + ' exact version' + (selectedKeys.length === 1 ? '' : 's') + ' selected in output order.' : shown.length ? 'Select media to continue.' : 'No matching media.';\\n};\\n\\nStillMade.onInput(values => {\\n  current = values;\\n  revision += 1;\\n  assets = (values.assets || []).slice(0, 500);\\n  kind.value = values.kind || 'All';\\n  const exact = (values.selectedVersions || '').split(/\\\\r?\\\\n/).map(value => value.trim()).filter(key => assets.some(asset => keyOf(asset) === key));\\n  const legacy = (values.selectedIds || '').split(/\\\\r?\\\\n/).map(value => value.trim()).filter(Boolean);\\n  selectedKeys = exact.length ? exact : legacy.length ? assets.filter(asset => legacy.includes(asset.assetId)).map(keyOf) : visible().map(keyOf);\\n  draw();\\n});\\nStillMade.onAction(event => {\\n  if (busy) return;\\n  const parts = event.action.split(':');\\n  const action = parts[0], index = Number(parts[1]);\\n  if (action === 'asset') {\\n    const asset = visible().find((item, position) => position === index);\\n    if (!asset) return;\\n    const key = keyOf(asset);\\n    selectedKeys = event.checked ? [...selectedKeys.filter(value => value !== key), key] : selectedKeys.filter(value => value !== key);\\n  }\\n  if (action === 'remove') selectedKeys = selectedKeys.filter((value, position) => position !== index);\\n  if (action === 'up' && index > 0) { const item = selectedKeys.find((value, position) => position === index), before = selectedKeys.find((value, position) => position === index - 1); selectedKeys = selectedKeys.map((value, position) => position === index - 1 ? item : position === index ? before : value); }\\n  if (action === 'down' && index < selectedKeys.length - 1) { const item = selectedKeys.find((value, position) => position === index), after = selectedKeys.find((value, position) => position === index + 1); selectedKeys = selectedKeys.map((value, position) => position === index ? after : position === index + 1 ? item : value); }\\n  draw();\\n});\\nkind.addEventListener('change', () => { if (!busy) { selectedKeys = visible().map(keyOf); draw(); } });\\nall.addEventListener('click', () => { if (!busy) { selectedKeys = visible().map(keyOf); draw(); } });\\nnone.addEventListener('click', () => { if (!busy) { selectedKeys = []; draw(); } });\\nuse.addEventListener('click', async () => {\\n  if (busy || !selectedKeys.length) return;\\n  const started = revision;\\n  busy = true;\\n  draw();\\n  let message;\\n  try {\\n    const result = await StillMade.run({assets: current.assets || [], kind: kind.value, selectedIds: '', selectedVersions: selectedKeys.join('\\\\n')});\\n    message = String((result.output || []).length) + ' exact media versions are ready for the next step.';\\n  } catch (error) { message = error.message || 'The selection could not be prepared.'; }\\n  finally { busy = false; draw(); if (started === revision) status.textContent = message; }\\n});\\n\"\n}"},"lineage":{"parents":[],"changes":"Packaged original Block source for StillMade."}}},{"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"sm.pipeline","version":"2.2.2","name":"Canvas","description":"Create and edit the complete Canvas, choose exact saved generation versions, retain history, and propose a reviewed project save while preserving Project Type Steps and connections.","kind":"workspace","runtime":"javascript","entry":"src/run.js","license":"MIT","provenance":{"notice":"MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"},"inputs":{"canvas":{"type":"pipeline","context":"canvas","primary":true,"required":false,"default":{"schemaVersion":1,"nodes":[],"edges":[]},"description":"The complete safe Canvas projection. Native content and layout are retained; executable source and workflow history remain host-owned."},"assets":{"type":"asset[]","context":"assets","required":false,"description":"Current saved project media available to add to Canvas."},"versions":{"type":"metadata[]","context":"versions","required":false,"description":"Saved project version history available for exact take selection."},"updatedCanvas":{"type":"pipeline","required":false,"default":{"schemaVersion":1,"nodes":[],"edges":[]},"description":"The complete edited Canvas projection prepared in this workspace."},"commit":{"type":"boolean","default":false,"description":"Prepare the edited Canvas for StillMade review and Apply."},"generatedImage":{"type":"image","imageMode":"reference","required":false,"description":"An exact stored image version produced by an approved generation Block or selected from project media."},"addGeneratedImage":{"type":"boolean","default":false,"description":"Add the connected image as a native Canvas media node."},"generatedImageTitle":{"type":"text","required":false,"default":"Generated image","description":"Label for the connected image on Canvas."},"generatedVideo":{"type":"video","required":false,"description":"An exact stored video version produced by an approved generation Block or selected from project media."},"addGeneratedVideo":{"type":"boolean","default":false,"description":"Add the connected video as a native Canvas media node."},"generatedVideoTitle":{"type":"text","required":false,"default":"Generated video","description":"Label for the connected video on Canvas."},"generatedAudio":{"type":"audio","required":false,"description":"An exact stored narration version produced by an approved Voiceover Block or selected from project media."},"addGeneratedAudio":{"type":"boolean","default":false,"description":"Add the connected narration as a native Canvas voiceover node."},"generatedAudioTitle":{"type":"text","required":false,"default":"Voiceover","description":"Label for the connected narration on Canvas."}},"outputs":{"canvas":{"type":"pipeline","primary":true,"role":"canvas_document","description":"The complete edited safe Canvas projection."},"edit":{"type":"workspace-edit","description":"An exact before/after Canvas change reviewed before it updates the project."}},"permissions":{"project":["context.canvas.read","context.assets.read","context.versions.read","asset.read","workspace.canvas.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[],"platforms":{"phone":{"supported":true},"browser":{"supported":true},"desktop":{"supported":true}}},"code":"const copy=value=>JSON.parse(JSON.stringify(value));\nconst source=copy(input.canvas||{schemaVersion:1,nodes:[],edges:[]});\nconst candidate=input.commit?copy(input.updatedCanvas):copy(source);\nif(!candidate||Array.isArray(candidate)||candidate.schemaVersion!==1||!Array.isArray(candidate.nodes)||!Array.isArray(candidate.edges))throw new Error('Keep the complete version 1 Canvas document with nodes and edges.');\nconst attach=(value,kind,title)=>{\n if(!value||value.kind!==kind)throw new Error('Connect an approved generated '+kind+' before adding it to Canvas.');\n const ref=copy(value),base='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId;\n let id=base,suffix=2;while(candidate.nodes.some(node=>node.id===id)){id=base+'-'+suffix;suffix+=1}\n const position={x:80+candidate.nodes.length*24,y:80+candidate.nodes.length*24};\n if(kind==='image')candidate.nodes.push({id,type:'imageGenNode',position,width:320,height:320,data:{title:title||'Generated image',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='video')candidate.nodes.push({id,type:'videoGenNode',position,width:360,height:260,data:{title:title||'Generated video',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='audio')candidate.nodes.push({id,type:'voiceoverGenNode',position,width:360,height:260,data:{title:title||'Voiceover',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}});\n};\nif(input.addGeneratedImage)attach(input.generatedImage,'image',input.generatedImageTitle);\nif(input.addGeneratedVideo)attach(input.generatedVideo,'video',input.generatedVideoTitle);\nif(input.addGeneratedAudio)attach(input.generatedAudio,'audio',input.generatedAudioTitle);\nif(candidate.nodes.length>5000||candidate.edges.length>10000)throw new Error('Canvas supports up to 5,000 nodes and 10,000 connections.');\nif(!candidate.nodes.every(node=>node&&typeof node==='object'&&!Array.isArray(node)&&typeof node.id==='string'&&node.id&&typeof node.type==='string'&&node.type&&node.position&&Number.isFinite(node.position.x)&&Number.isFinite(node.position.y)))throw new Error('Every Canvas node needs an identity, type, and finite position.');\nif(new Set(candidate.nodes.map(node=>node.id)).size!==candidate.nodes.length)throw new Error('Canvas node identities must be distinct.');\nconst ids=new Set(candidate.nodes.map(node=>node.id));\nif(!candidate.edges.every(edge=>edge&&typeof edge==='object'&&!Array.isArray(edge)&&typeof edge.id==='string'&&edge.id&&ids.has(edge.source)&&ids.has(edge.target)))throw new Error('Every Canvas connection needs an identity and existing source and target nodes.');\nif(new Set(candidate.edges.map(edge=>edge.id)).size!==candidate.edges.length)throw new Error('Canvas connection identities must be distinct.');\nreturn{canvas:candidate,edit:{schemaVersion:1,title:`Update Canvas with ${candidate.nodes.length} node${candidate.nodes.length===1?'':'s'}`,field:'canvas',before:source,after:candidate}};\n","view":{"html":"<main class=\"canvas-workspace\">\n  <header><div><small>Production workspace</small><h1>Canvas</h1><p id=\"summary\">Loading the complete Canvas…</p></div><div class=\"actions\"><button id=\"reset\" type=\"button\" class=\"secondary\">Reset changes</button><button id=\"add\" type=\"button\" class=\"secondary\">Add note</button><button id=\"save\" data-stillmade-action=\"run\" type=\"button\">Review changes</button></div></header>\n  <output id=\"status\" role=\"status\" aria-live=\"polite\"></output>\n  <section><div class=\"section-title\"><h2>Connected media</h2></div><div class=\"handoffs\"><div id=\"handoff-image\"></div><div id=\"handoff-video\"></div><div id=\"handoff-audio\"></div></div></section>\n  <section><div class=\"section-title\"><h2>Saved media and history</h2><p class=\"hint\">Choose any exact saved version and add it to Canvas.</p></div><div id=\"library\" class=\"cards\"></div></section>\n  <section><h2>Canvas items</h2><p class=\"hint\">Project Type Steps stay connected and carry their exact installed source. Edit their placement or label here; use the Project Type builder to add, remove, replace, or reconnect Steps.</p><div id=\"nodes\" class=\"cards\"></div></section>\n  <section><h2>Connections</h2><div id=\"edges\" class=\"connections\"></div></section>\n  <details><summary>Complete Canvas data</summary><p>Native content, media references, dimensions, and custom fields are retained. Executable source and host bindings are protected when StillMade applies the proposal.</p><label for=\"raw\" class=\"raw-label\">Canvas JSON</label><textarea id=\"raw\" class=\"raw\" spellcheck=\"false\"></textarea><button id=\"applyRaw\" type=\"button\" class=\"secondary\">Apply JSON to workspace</button></details>\n</main>\n","css":".canvas-workspace{max-width:1120px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}header{justify-content:space-between;margin-bottom:14px}header h1{margin:2px 0}header p,header small,details p,output,.hint{color:var(--fg-muted)}section,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}section h2{margin:0 0 8px}.cards,.connections{display:grid;gap:10px}.handoffs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.handoff,.card,.connection{padding:12px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.handoff p{min-height:38px}.card-head{justify-content:space-between}.card h3{margin:0;font-size:16px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}label{display:grid;gap:5px;margin:7px 0;font-size:13px;font-weight:600}input,textarea{width:100%;background:var(--bg)}textarea{min-height:76px;resize:vertical}.wide{grid-column:1/-1}.secondary{background:transparent;color:var(--fg)}.danger{background:transparent;color:var(--signal)}.raw{min-height:420px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}.badge{font-size:12px;color:var(--fg-muted)}\n@media(max-width:760px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.canvas-workspace{padding:0}.grid,.handoffs{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}section,details{padding:12px}}\n\n#library .handoff{min-width:0}#library .handoff button{width:100%}\n","javascript":"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save');\nlet source={schemaVersion:1,nodes:[],edges:[]},draft={schemaVersion:1,nodes:[],edges:[]},connectedImage=null,connectedVideo=null,connectedAudio=null,titles={image:'Generated image',video:'Generated video',audio:'Voiceover'},library=[],busy=false,revision=0;\nconst clone=value=>JSON.parse(JSON.stringify(value));\nconst text=value=>value===undefined||value===null?'':String(value);\nconst managed=node=>node.type==='programmableBlockNode'||!!(node.data&&node.data.hostBlock);\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\nconst nodeCard=(node,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(node.data&&node.data.title)||text(node.data&&node.data.label)||text(node.data&&node.data.name)||text(node.id)},{tag:'span',className:'badge',text:managed(node)?'Project Type Step':text(node.type)},{tag:'button',action:'remove:'+String(index),text:'Remove',className:'danger',disabled:busy||managed(node)}]},{tag:'div',className:'grid',children:[field('Title','title:'+String(index),node.data&&node.data.title),field('Label','label:'+String(index),node.data&&node.data.label),field('X','x:'+String(index),node.position&&node.position.x,'number'),field('Y','y:'+String(index),node.position&&node.position.y,'number'),field('Width','width:'+String(index),node.width,'number'),field('Height','height:'+String(index),node.height,'number'),managed(node)?{tag:'p',className:'wide hint',text:'Its executable source and connections are preserved.'}:field('Content','content:'+String(index),node.data&&(node.data.text===undefined?node.data.content:node.data.text),'text',true)]}]});\nconst edgeCard=edge=>({tag:'article',className:'connection',children:[{tag:'strong',text:text(edge.source)+' → '+text(edge.target)},{tag:'p',className:'hint',text:text(edge.sourceHandle)+' '+text(edge.targetHandle)}]});\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft=clone(source);if(!Array.isArray(draft.nodes))draft.nodes=[];if(!Array.isArray(draft.edges))draft.edges=[]};\nconst libraryCard=(value,index)=>({tag:'article',className:'handoff',children:[{tag:'strong',text:text(value.label)||text(value.assetId)},{tag:'p',className:'hint',text:text(value.kind)+' · version '+text(value.versionId)},{tag:'button',action:'library-'+text(value.kind)+':'+String(index),text:'Add to Canvas',className:'secondary',disabled:busy}]});\nconst handoff=(kind,value,label)=>value?[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:text(value.assetId)+' · version '+text(value.versionId)},{tag:'button',action:'attach-'+kind,text:'Add '+kind+' to Canvas',className:'secondary',disabled:busy}]}]:[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:'Connect a stored '+kind+' output to add it with exact asset and version identity.'}]}];\nconst draw=()=>{ensure();StillMade.render('handoff-image',handoff('image',connectedImage,'Image'));StillMade.render('handoff-video',handoff('video',connectedVideo,'Video'));StillMade.render('handoff-audio',handoff('audio',connectedAudio,'Narration'));StillMade.render('library',library.length?library.map(libraryCard):[{tag:'p',text:'No saved media versions yet.'}]);StillMade.render('nodes',draft.nodes.length?draft.nodes.map(nodeCard):[{tag:'p',text:'No Canvas items yet.'}]);StillMade.render('edges',draft.edges.length?draft.edges.map(edgeCard):[{tag:'p',text:'No connections.'}]);raw.value=JSON.stringify(draft,null,2);summary.textContent=String(draft.nodes.length)+' item'+(draft.nodes.length===1?'':'s')+' · '+String(draft.edges.length)+' connection'+(draft.edges.length===1?'':'s');save.disabled=busy;document.getElementById('reset').disabled=busy;document.getElementById('add').disabled=busy};\nconst changed=()=>{revision+=1;raw.value=JSON.stringify(draft,null,2);status.textContent='Changes are local until you choose Review changes.'};\nconst attach=(kind,ref,title)=>{const id='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId+'-'+String(revision+1),position={x:80+draft.nodes.length*24,y:80+draft.nodes.length*24};let node;if(kind==='image')node={id,type:'imageGenNode',position,width:320,height:320,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}};if(kind==='video')node={id,type:'videoGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}};if(kind==='audio')node={id,type:'voiceoverGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}};draft.nodes=[...draft.nodes,node];changed();draw();status.textContent='Connected '+kind+' added with its exact asset and version identity.'};\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],index=Number(parts[1]);if(kind==='attach-image'&&connectedImage){attach('image',connectedImage,titles.image);return}if(kind==='attach-video'&&connectedVideo){attach('video',connectedVideo,titles.video);return}if(kind==='attach-audio'&&connectedAudio){attach('audio',connectedAudio,titles.audio);return}if(kind.startsWith('library-')){const value=library.find((item,position)=>position===index),mediaKind=kind.slice(8);if(value&&value.kind===mediaKind){attach(mediaKind,value,text(value.label)||text(value.assetId));return}}const node=draft.nodes.find((item,position)=>position===index);if(!node)return;if(kind==='remove'&&!managed(node)){const id=node.id;draft.nodes=draft.nodes.filter((item,position)=>position!==index);draft.edges=draft.edges.filter(edge=>edge.source!==id&&edge.target!==id);changed();draw();return}if(!node.data||Array.isArray(node.data))node.data={};if(kind==='title')node.data.title=event.value;if(kind==='label')node.data.label=event.value;if(kind==='x')node.position.x=Number(event.value)||0;if(kind==='y')node.position.y=Number(event.value)||0;if(kind==='width')node.width=Math.max(1,Number(event.value)||1);if(kind==='height')node.height=Math.max(1,Number(event.value)||1);if(kind==='content'&&!managed(node)){if(node.data.text!==undefined)node.data.text=event.value;else node.data.content=event.value}changed()});\ndocument.getElementById('add').addEventListener('click',()=>{if(busy)return;ensure();const id='portable-note-'+String(draft.nodes.length+1)+'-'+String(revision+1);draft.nodes=[...draft.nodes,{id,type:'stickyNote',position:{x:80+draft.nodes.length*24,y:80+draft.nodes.length*24},width:260,height:180,data:{title:'Note',text:'New note'}}];changed();draw()});\ndocument.getElementById('applyRaw').addEventListener('click',()=>{if(busy)return;try{const value=JSON.parse(raw.value);if(!value||Array.isArray(value)||value.schemaVersion!==1||!Array.isArray(value.nodes)||!Array.isArray(value.edges))throw Error('Canvas JSON must be a version 1 document with nodes and edges.');draft=clone(value);changed();draw();status.textContent='Complete Canvas data applied locally.'}catch(error){status.textContent=error.message}});\ndocument.getElementById('reset').addEventListener('click',()=>{if(!busy){draft=clone(source);revision+=1;draw();status.textContent='Changes reset to the current Canvas.'}});\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Canvas…';try{const result=await StillMade.run({canvas:source,updatedCanvas:draft,commit:true});draft=clone(result.canvas);if(started===revision)status.textContent='Canvas changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'Canvas changes could not be prepared.'}finally{busy=false;draw()}});\nStillMade.onInput(values=>{source=clone(values.canvas||source);draft=clone(source);connectedImage=values.generatedImage?clone(values.generatedImage):null;connectedVideo=values.generatedVideo?clone(values.generatedVideo):null;connectedAudio=values.generatedAudio?clone(values.generatedAudio):null;const current=Array.isArray(values.assets)?values.assets:[],historic=(Array.isArray(values.versions)?values.versions:[]).map(item=>item&&item.asset).filter(Boolean);library=[...current,...historic].filter((item,index,list)=>item&&['image','video','audio'].includes(item.kind)&&list.findIndex(other=>other&&other.kind===item.kind&&other.assetId===item.assetId&&other.versionId===item.versionId)===index).slice(0,120);titles={image:values.generatedImageTitle||'Generated image',video:values.generatedVideoTitle||'Generated video',audio:values.generatedAudioTitle||'Voiceover'};revision+=1;draw();status.textContent='Edit the complete Canvas. Project Type Steps remain protected as whole Blocks.'});\n"},"tests":[{"name":"Retain native media and protected Steps","input":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"updatedCanvas":{"schemaVersion":1,"nodes":[],"edges":[]},"commit":false},"expected":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"edit":{"schemaVersion":1,"title":"Update Canvas with 2 nodes","field":"canvas","before":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"after":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]}}}},{"name":"Prepare one exact Canvas update","input":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"data":{"title":"Whiteboard"}},{"id":"note","type":"stickyNote","position":{"x":400,"y":20},"data":{"text":"Old"}}],"edges":[]},"updatedCanvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]},"commit":true},"expected":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]},"edit":{"schemaVersion":1,"title":"Update Canvas with 2 nodes","field":"canvas","before":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"data":{"title":"Whiteboard"}},{"id":"note","type":"stickyNote","position":{"x":400,"y":20},"data":{"text":"Old"}}],"edges":[]},"after":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]}}}},{"name":"Attach exact generated media versions","input":{"canvas":{"schemaVersion":1,"nodes":[],"edges":[]},"updatedCanvas":{"schemaVersion":1,"nodes":[],"edges":[]},"commit":false,"generatedImage":{"kind":"image","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png"},"addGeneratedImage":true,"generatedImageTitle":"Opening frame","generatedVideo":{"kind":"video","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","mimeType":"video/mp4","width":1280,"height":720,"duration":6,"bytes":120000},"addGeneratedVideo":true,"generatedVideoTitle":"Opening motion","generatedAudio":{"kind":"audio","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","mimeType":"audio/wav","duration":12,"bytes":576044,"sampleRate":24000,"channels":1},"addGeneratedAudio":true,"generatedAudioTitle":"Main narration"},"expected":{"canvas":{"schemaVersion":1,"nodes":[{"id":"connected-image-image-asset-image-version","type":"imageGenNode","position":{"x":80,"y":80},"width":320,"height":320,"data":{"title":"Opening frame","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png","versions":[{"id":"image-version","url":"https://media.example/frame.png","assetId":"image-asset"}],"activeVersion":0,"selectedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-video-video-asset-video-version","type":"videoGenNode","position":{"x":104,"y":104},"width":360,"height":260,"data":{"title":"Opening motion","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","duration":6,"versions":[{"id":"video-version","url":"https://media.example/clip.mp4","assetId":"video-asset","duration":6}],"currentVersion":0,"approvedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-audio-audio-asset-audio-version","type":"voiceoverGenNode","position":{"x":128,"y":128},"width":360,"height":260,"data":{"title":"Main narration","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","duration":12,"versions":[{"id":"audio-version","url":"https://media.example/voice.wav","assetId":"audio-asset","duration":12}],"currentVersion":0,"status":"complete","source":"connected-block"}}],"edges":[]},"edit":{"schemaVersion":1,"title":"Update Canvas with 3 nodes","field":"canvas","before":{"schemaVersion":1,"nodes":[],"edges":[]},"after":{"schemaVersion":1,"nodes":[{"id":"connected-image-image-asset-image-version","type":"imageGenNode","position":{"x":80,"y":80},"width":320,"height":320,"data":{"title":"Opening frame","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png","versions":[{"id":"image-version","url":"https://media.example/frame.png","assetId":"image-asset"}],"activeVersion":0,"selectedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-video-video-asset-video-version","type":"videoGenNode","position":{"x":104,"y":104},"width":360,"height":260,"data":{"title":"Opening motion","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","duration":6,"versions":[{"id":"video-version","url":"https://media.example/clip.mp4","assetId":"video-asset","duration":6}],"currentVersion":0,"approvedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-audio-audio-asset-audio-version","type":"voiceoverGenNode","position":{"x":128,"y":128},"width":360,"height":260,"data":{"title":"Main narration","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","duration":12,"versions":[{"id":"audio-version","url":"https://media.example/voice.wav","assetId":"audio-asset","duration":12}],"currentVersion":0,"status":"complete","source":"connected-block"}}],"edges":[]}}}}]},{"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"sm.editor","version":"2.2.7","name":"Editor","description":"Build and render a complete editable timeline, including coded vector scenes whose transforms may be omitted or retained in the scene document.","kind":"workspace","runtime":"javascript","entry":"src/run.js","license":"MIT","provenance":{"notice":"Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js, src/view.json\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","portableSources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}]},"inputs":{"editor":{"type":"timeline","context":"editor","primary":true,"required":false,"default":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"description":"The complete current Editor document. All internal editing tools and retained media/version data remain together."},"assets":{"type":"asset[]","context":"assets","required":false,"default":[],"description":"Current project media used to assemble an initial editable timeline when the Editor is empty."},"updatedEditor":{"type":"timeline","required":false,"default":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"description":"The complete edited timeline prepared in this workspace."},"commit":{"type":"boolean","default":false,"description":"Prepare the edited Editor document for StillMade review and Apply."},"action":{"type":"text","required":false,"default":"none","description":"The Block-owned interface selects a declared host action."},"renderFps":{"type":"integer","min":12,"max":60,"default":30,"description":"Frames per second sent to the declared final-render service."},"renderResolution":{"type":"integer","min":480,"max":2160,"default":1080,"description":"Output height sent to the declared final-render service."},"renderQuality":{"type":"text","default":"high","description":"Output quality sent to the declared final-render service."}},"outputs":{"editor":{"type":"timeline","primary":true,"role":"editor_timeline","description":"The complete edited timeline for immediate use and downstream Blocks."},"edit":{"type":"workspace-edit","description":"An exact before/after Editor change reviewed before it updates the project."},"mediaAction":{"type":"approval","required":false,"role":"portable_media_action","description":"A bounded final-render request executed only after the user chooses Render and play."}},"permissions":{"project":["context.editor.read","context.assets.read","workspace.editor.propose"],"network":[],"filesystem":[],"secrets":[],"actions":["final.render"]},"ui":[],"platforms":{"phone":{"supported":true},"browser":{"supported":true},"desktop":{"supported":true}}},"code":"const copy = value => JSON.parse(JSON.stringify(value));\nconst source = copy(input.editor || {schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]});\nconst assets=Array.isArray(input.assets)?input.assets.filter(asset=>asset&&['image','video','audio'].includes(asset.kind)&&asset.assetId&&asset.versionId&&asset.url).slice(0,500):[];\nconst assembled=copy(source);if(!assembled.clips?.length&&!assembled.audioItems?.length&&assets.length){assembled.tracks=[{id:'video',kind:'video',label:'Video'},{id:'audio',kind:'audio',label:'Audio'}];let cursor=0;assembled.clips=assets.filter(asset=>asset.kind!=='audio').map((asset,index)=>{const duration=Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||4),clip={id:'clip-'+String(index+1)+'-'+String(asset.assetId),trackId:'video',start:cursor,duration,trimStart:0,speed:1,volume:1,asset:copy(asset),url:asset.url};cursor+=duration;return clip});assembled.audioItems=assets.filter(asset=>asset.kind==='audio').map((asset,index)=>({id:'audio-'+String(index+1)+'-'+String(asset.assetId),trackId:'audio',start:0,duration:Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||cursor||1),trimStart:0,volume:1,asset:copy(asset),url:asset.url}));assembled.targetDuration=Math.max(cursor,...assembled.audioItems.map(item=>item.duration));}\nconst candidate = input.commit || input.action === 'render' ? copy(input.updatedEditor) : assembled;\nif (!candidate || Array.isArray(candidate) || candidate.schemaVersion !== 1 || typeof candidate.activeTimelineId !== 'string' || !candidate.activeTimelineId) throw new Error('Keep the complete version 1 Editor document and active timeline identity.');\nconst names=['tracks','clips','audioItems','captions','textOverlays','stickers','effects','markers','fxEvents','liveOverlays','mediaItems','timelineVersions'];\nif (!names.every(name => candidate[name] === undefined || Array.isArray(candidate[name]) && candidate[name].length <= 10000 && candidate[name].every(item => item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string' && item.id.length > 0 && item.id.length <= 200) && new Set(candidate[name].map(item => item.id)).size === candidate[name].length)) throw new Error('Editor collections need valid distinct retained item IDs.');\nconst itemEnd=item=>Math.max(0,Number(item.start)||0)+Math.max(.001,Number(item.duration)||Number(item.durationMs)/1000||.001);\nconst outputs={editor:candidate,edit:{schemaVersion:1,title:`Update Editor timeline ${candidate.activeTimelineId}`,field:'editor',before:source,after:candidate}};\nif(input.action==='render'){const clips=Array.isArray(candidate.clips)?candidate.clips:[],audio=Array.isArray(candidate.audioItems)?candidate.audioItems:[],overlays=[...(candidate.captions||[]),...(candidate.textOverlays||[]),...(candidate.effects||[])],targetDuration=Math.max(.001,Number(candidate.targetDuration)||0,...clips.map(itemEnd),...audio.map(itemEnd),...overlays.map(itemEnd));outputs.mediaAction={schemaVersion:1,kind:'stillmade.media-action',operation:'final.render',request:{schemaVersion:1,kind:'video',timeline:{...candidate,clips,audio,overlays,targetDuration},settings:{fps:input.renderFps,resolution:input.renderResolution,quality:input.renderQuality},source:{id:'sm.editor',version:'2.2.7'}},resultPort:null};}\nreturn outputs;\n","view":{"html":"<main class=\"editor-workspace\">\n  <header><div><small>Production workspace</small><h1>Editor</h1><p id=\"summary\">Loading the complete timeline…</p></div><div class=\"actions\"><button id=\"build\" type=\"button\" class=\"secondary\">Build from project media</button><button id=\"undo\" type=\"button\" class=\"secondary\">Undo</button><button id=\"redo\" type=\"button\" class=\"secondary\">Redo</button><button id=\"snapshot\" type=\"button\" class=\"secondary\">Save version</button><button id=\"reset\" type=\"button\" class=\"secondary\">Reset changes</button><button id=\"addText\" type=\"button\" class=\"secondary\">Add text</button><button id=\"addCaption\" type=\"button\" class=\"secondary\">Add caption</button><button id=\"save\" type=\"button\" data-stillmade-action=\"run\">Review changes</button></div></header>\n  <output id=\"status\" role=\"status\" aria-live=\"polite\"></output>\n  <section><h2>Timeline</h2><div class=\"grid\"><label>Active timeline<input id=\"timelineId\" type=\"text\" disabled></label><label>Aspect ratio<input id=\"aspect\" type=\"text\"></label><label>Playhead (seconds)<input id=\"playhead\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" value=\"0\"></label></div><p id=\"timecode\" class=\"hint\"></p><div id=\"composition\" class=\"cards\"></div></section>\n  <section><h2>Project media</h2><p class=\"hint\">Preview an exact saved version, then add it to this timeline without leaving the Block.</p><img id=\"imagePreview\" alt=\"Selected project media preview\" hidden><video id=\"videoPreview\" controls hidden></video><audio id=\"audioPreview\" controls hidden></audio><div id=\"assets\" class=\"cards media-grid\"></div></section>\n  <section><h2>Video</h2><div id=\"clips\" class=\"cards\"></div></section>\n  <section><h2>Audio and scoring</h2><div id=\"audio\" class=\"cards\"></div></section>\n  <section><h2>Saved timeline versions</h2><div id=\"versions\" class=\"cards\"></div></section>\n  <section><h2>Text and motion graphics</h2><div id=\"text\" class=\"cards\"></div><p id=\"graphics\" class=\"hint\"></p></section>\n  <section><h2>Captions</h2><div id=\"captions\" class=\"cards\"></div></section>\n  <section><div class=\"card-head\"><h2>Effects</h2><button id=\"addEffect\" type=\"button\" class=\"secondary\">Add effect</button></div><div id=\"effects\" class=\"cards\"></div></section>\n  <details><summary>Complete Editor data</summary><p>All timeline lanes, motion graphics, grade settings, transitions, scoring, versions, and export settings are retained. Use this editor for fields not shown on the lane cards.</p><textarea id=\"raw\" class=\"raw\" spellcheck=\"false\" aria-label=\"Complete Editor data\"></textarea><button id=\"applyRaw\" type=\"button\" class=\"secondary\">Apply JSON to workspace</button></details>\n<section><h2>Final export</h2><p>Render this exact timeline through StillMade’s approved device renderer.</p><div class=\"grid\"><label>Frame rate<select id=\"renderFps\"><option value=\"24\">24 fps</option><option value=\"30\" selected>30 fps</option><option value=\"60\">60 fps</option></select></label><label>Resolution<select id=\"renderResolution\"><option value=\"720\">720p</option><option value=\"1080\" selected>1080p</option><option value=\"2160\">2160p</option></select></label><label>Quality<select id=\"renderQuality\"><option value=\"standard\">Standard</option><option value=\"high\" selected>High</option></select></label></div><button id=\"renderFinal\" type=\"button\">Render final video</button></section></main>\n","css":".editor-workspace{max-width:1160px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head,.row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}header{justify-content:space-between;margin-bottom:14px}header h1{margin:2px 0}header p,header small,details p,output,.hint{color:var(--fg-muted)}section,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}section h2{margin:0 0 10px}.cards{display:grid;gap:10px}.card{padding:12px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.card-head{justify-content:space-between}.card h3{margin:0;font-size:16px}.grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}label{display:grid;gap:5px;margin:7px 0;font-size:13px;font-weight:600}input,textarea{width:100%;background:var(--bg)}textarea{min-height:70px;resize:vertical}.wide{grid-column:1/-1}.secondary{background:transparent;color:var(--fg)}.danger{background:transparent;color:var(--signal)}.raw{min-height:440px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}\n@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.editor-workspace{padding:0}.grid{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}section,details{padding:12px}}\n.media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.media-card p{margin:4px 0;color:var(--fg-muted)}video,audio{display:block;width:100%;margin:10px 0}video[hidden],audio[hidden]{display:none}select{width:100%;background:var(--bg)}\n","javascript":"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save'),aspect=document.getElementById('aspect'),timelineId=document.getElementById('timelineId'),graphics=document.getElementById('graphics'),build=document.getElementById('build'),imagePreview=document.getElementById('imagePreview'),videoPreview=document.getElementById('videoPreview'),audioPreview=document.getElementById('audioPreview'),playhead=document.getElementById('playhead'),timecode=document.getElementById('timecode');\nlet source={schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]},draft={schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]},assets=[],previewUrl='',busy=false,revision=0,editStack=[],editStackIndex=-1;\nconst clone=value=>JSON.parse(JSON.stringify(value));\nconst text=value=>value===undefined||value===null?'':String(value);\nconst assemble=()=>{const next=clone(source),media=assets.filter(asset=>asset&&['image','video','audio'].includes(asset.kind)&&asset.assetId&&asset.versionId&&asset.url).slice(0,500);if(next.clips?.length||next.audioItems?.length||!media.length)return next;next.tracks=[{id:'video',kind:'video',label:'Video'},{id:'audio',kind:'audio',label:'Audio'}];let cursor=0;next.clips=media.filter(asset=>asset.kind!=='audio').map((asset,index)=>{const duration=Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||4),clip={id:'clip-'+String(index+1)+'-'+text(asset.assetId),trackId:'video',start:cursor,duration,trimStart:0,speed:1,volume:1,asset:clone(asset),url:asset.url};cursor+=duration;return clip});next.audioItems=media.filter(asset=>asset.kind==='audio').map((asset,index)=>({id:'audio-'+String(index+1)+'-'+text(asset.assetId),trackId:'audio',start:0,duration:Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||cursor||1),trimStart:0,volume:1,asset:clone(asset),url:asset.url}));next.targetDuration=Math.max(cursor,...next.audioItems.map(item=>item.duration));return next};\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\nconst remove=action=>({tag:'button',action,text:'Remove',className:'danger',disabled:busy});\nconst assetCard=(item,index)=>({tag:'article',className:'card media-card',children:[{tag:'strong',text:text(item.name)||text(item.assetId)},{tag:'p',text:text(item.kind)+' · version '+text(item.versionId)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-asset:'+String(index),text:'Preview',className:'secondary',disabled:busy},{tag:'button',action:'add-asset:'+String(index),text:item.kind==='audio'?'Add audio':'Add clip',className:'secondary',disabled:busy}]}]});\nconst clipCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.label)||text(item.name)||text(item.id)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-clip:'+String(index),text:'Preview',className:'secondary',disabled:busy||!(item.asset||item.assetId)},{tag:'button',action:'move-clip-up:'+String(index),text:'Up',className:'secondary',disabled:busy||index===0},{tag:'button',action:'move-clip-down:'+String(index),text:'Down',className:'secondary',disabled:busy||index===draft.clips.length-1},{tag:'button',action:'split-clip:'+String(index),text:'Split',className:'secondary',disabled:busy||Number(item.duration)<=0.002},{tag:'button',action:'duplicate-clip:'+String(index),text:'Duplicate',className:'secondary',disabled:busy},remove('remove-clip:'+String(index))]}]},{tag:'div',className:'grid',children:[field('Track','clip-track:'+String(index),item.trackId),field('Start (seconds)','clip-start:'+String(index),item.start,'number'),field('Length (seconds)','clip-duration:'+String(index),item.duration,'number'),field('Trim start','clip-trim:'+String(index),item.trimStart,'number'),field('Speed','clip-speed:'+String(index),item.speed,'number'),field('Volume','clip-volume:'+String(index),item.volume,'number'),field('Transition','clip-transition:'+String(index),item.transition&&item.transition.type),field('Transition length','clip-transition-duration:'+String(index),item.transition&&item.transition.duration,'number'),field('Motion','clip-motion:'+String(index),item.motion&&item.motion.type),field('Motion easing','clip-easing:'+String(index),item.motion&&item.motion.easing),field('Opacity','clip-opacity:'+String(index),item.transform&&item.transform.opacity!==undefined?item.transform.opacity:item.opacity,'number'),{tag:'label',children:[{tag:'span',text:'Reverse playback'},{tag:'input',type:'checkbox',action:'clip-reverse:'+String(index),checked:!!item.reverse,disabled:busy,label:'Reverse playback'}]}]}]});\nconst audioCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.label)||text(item.name)||text(item.id)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-audio:'+String(index),text:'Preview',className:'secondary',disabled:busy||!(item.asset||item.assetId)},remove('remove-audio:'+String(index))]}]},{tag:'div',className:'grid',children:[field('Track','audio-track:'+String(index),item.trackId),field('Start (seconds)','audio-start:'+String(index),item.start,'number'),field('Length (seconds)','audio-duration:'+String(index),item.duration,'number'),field('Trim start','audio-trim:'+String(index),item.trimStart===undefined?item.sourceOffset:item.trimStart,'number'),field('Volume','audio-volume:'+String(index),item.volume,'number'),field('Fade in','audio-fade-in:'+String(index),item.fadeIn,'number'),field('Fade out','audio-fade-out:'+String(index),item.fadeOut,'number'),field('Ducking','audio-ducking:'+String(index),item.ducking,'number'),field('Playback speed','audio-rate:'+String(index),item.rate,'number'),{tag:'label',children:[{tag:'span',text:'Loop source'},{tag:'input',type:'checkbox',action:'audio-loop:'+String(index),checked:!!item.loop,disabled:busy,label:'Loop source'}]},{tag:'label',children:[{tag:'span',text:'Muted'},{tag:'input',type:'checkbox',action:'audio-muted:'+String(index),checked:!!item.muted,disabled:busy,label:'Muted'}]}]}]});\nconst textCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.id)},remove('remove-text:'+String(index))]},{tag:'div',className:'grid',children:[field('Text','text-value:'+String(index),item.text,'text',true),field('Track','text-track:'+String(index),item.trackId),field('Start (seconds)','text-start:'+String(index),item.start,'number'),field('Length (seconds)','text-duration:'+String(index),item.duration,'number'),field('Animation','text-animation:'+String(index),item.animation)]}]});\nconst captionCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.id)},remove('remove-caption:'+String(index))]},{tag:'div',className:'grid',children:[field('Caption','caption-value:'+String(index),item.text,'text',true),field('Track','caption-track:'+String(index),item.trackId),field('Start (seconds)','caption-start:'+String(index),item.start,'number'),field('End (seconds)','caption-end:'+String(index),item.end,'number'),field('Animation','caption-animation:'+String(index),item.animation)]}]});\nconst effectCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.type)||text(item.id)},remove('remove-effect:'+String(index))]},{tag:'div',className:'grid',children:[field('Type','effect-type:'+String(index),item.type),field('Track','effect-track:'+String(index),item.trackId),field('Start (seconds)','effect-start:'+String(index),item.start,'number'),field('Length (seconds)','effect-duration:'+String(index),item.duration,'number'),field('Intensity','effect-intensity:'+String(index),item.intensity,'number')]}]});\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft=clone(source);if(!Array.isArray(draft.tracks))draft.tracks=[];if(!Array.isArray(draft.clips))draft.clips=[];if(!Array.isArray(draft.audioItems))draft.audioItems=[];if(!Array.isArray(draft.captions))draft.captions=[];if(!Array.isArray(draft.textOverlays))draft.textOverlays=[];if(!Array.isArray(draft.effects))draft.effects=[]};\nconst itemFinish=item=>(Number(item.start)||0)+Math.max(.001,Number(item.duration)||Number(item.end)-(Number(item.start)||0)||.001);const composition=()=>{const at=Number(playhead.value)||0,activeClips=draft.clips.filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item)),activeAudio=draft.audioItems.filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item)),activeOverlays=[...draft.textOverlays,...draft.captions,...draft.effects].filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item));StillMade.render('composition',[{tag:'strong',text:'At '+at.toFixed(2)+'s'},{tag:'p',text:(activeClips.length?activeClips.map(item=>text(item.label)||text(item.id)).join(', '):'No video')+' · '+String(activeAudio.length)+' audio · '+String(activeOverlays.length)+' overlays'}]);timecode.textContent='Playhead '+at.toFixed(2)+' seconds'};const versionCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'strong',text:text(item.label)||text(item.id)},{tag:'p',text:text(item.createdAt)},{tag:'button',action:'restore-version:'+String(index),text:'Restore this version',className:'secondary',disabled:busy}]});const draw=()=>{ensure();StillMade.render('assets',assets.length?assets.map(assetCard):[{tag:'p',text:'No saved project media is available.'}]);StillMade.render('clips',draft.clips.length?draft.clips.map(clipCard):[{tag:'p',text:'No video clips.'}]);StillMade.render('audio',draft.audioItems.length?draft.audioItems.map(audioCard):[{tag:'p',text:'No audio items.'}]);StillMade.render('text',draft.textOverlays.length?draft.textOverlays.map(textCard):[{tag:'p',text:'No text overlays.'}]);StillMade.render('captions',draft.captions.length?draft.captions.map(captionCard):[{tag:'p',text:'No captions.'}]);StillMade.render('versions',Array.isArray(draft.timelineVersions)&&draft.timelineVersions.length?draft.timelineVersions.map(versionCard):[{tag:'p',text:'No saved versions yet.'}]);StillMade.render('effects',draft.effects.length?draft.effects.map(effectCard):[{tag:'p',text:'No effects.'}]);timelineId.value=text(draft.activeTimelineId);const duration=Math.max(1,Number(draft.targetDuration)||0,...draft.clips.map(itemFinish),...draft.audioItems.map(itemFinish));playhead.max=String(duration);composition();aspect.value=text(draft.aspect);const mg=(Array.isArray(draft.fullscreenFrames)?draft.fullscreenFrames.length:0)+(Array.isArray(draft.figureSweeps)?draft.figureSweeps.length:0)+(Array.isArray(draft.graphs)?draft.graphs.length:0);graphics.textContent=String(mg)+' motion graphic item'+(mg===1?'':'s')+' retained in this Editor Block. Use Complete Editor data for advanced motion-graphics fields.';raw.value=JSON.stringify(draft,null,2);summary.textContent=String(draft.clips.length)+' video · '+String(draft.audioItems.length)+' audio · '+String(draft.captions.length)+' captions · '+String(draft.textOverlays.length)+' text';save.disabled=busy;build.disabled=busy||!assets.length;document.getElementById('reset').disabled=busy;document.getElementById('addText').disabled=busy;document.getElementById('addCaption').disabled=busy;document.getElementById('addEffect').disabled=busy;document.getElementById('undo').disabled=busy||editStackIndex<=0;document.getElementById('redo').disabled=busy||editStackIndex>=editStack.length-1;document.getElementById('snapshot').disabled=busy};\nconst changed=()=>{revision+=1;editStack=editStack.slice(0,editStackIndex+1);editStack=[...editStack,clone(draft)].slice(-50);editStackIndex=editStack.length-1;raw.value=JSON.stringify(draft,null,2);status.textContent='Changes are local until you choose Review changes.'};\naspect.addEventListener('input',()=>{draft.aspect=aspect.value;changed()});\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],index=Number(parts[1]),clip=draft.clips.find((item,position)=>position===index),audio=draft.audioItems.find((item,position)=>position===index),title=draft.textOverlays.find((item,position)=>position===index),caption=draft.captions.find((item,position)=>position===index),effect=draft.effects.find((item,position)=>position===index),asset=assets.find((item,position)=>position===index);\n if(kind==='preview-clip'&&clip){const value=clip.asset||clip;if(previewUrl)StillMade.releaseMedia(previewUrl);const request=value.kind==='image'?StillMade.previewImage(value):StillMade.previewMedia({...value,kind:value.kind||'video'});request.then(preview=>{imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.hidden=true;if(value.kind==='image'){imagePreview.src=preview.url;imagePreview.hidden=false}else{previewUrl=preview.url;videoPreview.src=preview.url;videoPreview.hidden=false;videoPreview.currentTime=Math.max(0,Number(clip.trimStart)||0)}status.textContent='Previewing '+(clip.label||clip.name||clip.id)+'.'}).catch(error=>{status.textContent=error.message});return} if(kind==='preview-audio'&&audio){const value=audio.asset||audio;if(previewUrl)StillMade.releaseMedia(previewUrl);StillMade.previewMedia(value).then(preview=>{previewUrl=preview.url;imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.src=preview.url;audioPreview.hidden=false;status.textContent='Previewing '+(audio.label||audio.name||audio.id)+'.'}).catch(error=>{status.textContent=error.message});return}if(kind==='restore-version'&&Array.isArray(draft.timelineVersions)){const version=draft.timelineVersions.find((item,position)=>position===index);if(version){draft={...draft,clips:clone(version.clips||[]),audioItems:clone(version.audioItems||[]),captions:clone(version.captions||[]),textOverlays:clone(version.textOverlays||[]),effects:clone(version.effects||[])};changed();draw();status.textContent='Saved timeline version restored locally.'}return}if(kind==='preview-asset'&&asset){if(previewUrl)StillMade.releaseMedia(previewUrl);imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.hidden=true;const request=asset.kind==='image'?StillMade.previewImage(asset):StillMade.previewMedia(asset);request.then(value=>{previewUrl=asset.kind==='image'?'':value.url;if(asset.kind==='image'){imagePreview.src=value.url;imagePreview.hidden=false}else if(asset.kind==='video'){videoPreview.src=value.url;videoPreview.hidden=false}else{audioPreview.src=value.url;audioPreview.hidden=false}status.textContent='Previewing '+(asset.name||asset.assetId)+'.'}).catch(error=>{status.textContent=error.message});return}if(kind==='add-asset'&&asset){const media=clone(asset),duration=Math.max(.001,Number(media.duration)||Number(media.durationSec)||4);if(media.kind==='audio')draft.audioItems=[...draft.audioItems,{id:'audio-'+String(revision+1)+'-'+text(media.assetId),trackId:'audio',start:0,duration,trimStart:0,volume:1,asset:media,url:media.url}];else{const start=draft.clips.reduce((end,item)=>Math.max(end,(Number(item.start)||0)+(Number(item.duration)||0)),0);draft.clips=[...draft.clips,{id:'clip-'+String(revision+1)+'-'+text(media.assetId),trackId:'video',start,duration,trimStart:0,speed:1,volume:1,asset:media,url:media.url}]};changed();draw();return}if(kind==='move-clip-up'&&index>0){const target=draft.clips.find((item,position)=>position===index-1);draft.clips=draft.clips.map((item,position)=>position===index-1?clip:position===index?target:item);changed();draw();return}if(kind==='move-clip-down'&&index<draft.clips.length-1){const target=draft.clips.find((item,position)=>position===index+1);draft.clips=draft.clips.map((item,position)=>position===index+1?clip:position===index?target:item);changed();draw();return}if(kind==='duplicate-clip'&&clip){const next={...clone(clip),id:text(clip.id)+'-copy-'+String(revision+1),start:(Number(clip.start)||0)+(Number(clip.duration)||0)};draft.clips=[...draft.clips.slice(0,index+1),next,...draft.clips.slice(index+1)];changed();draw();return}if(kind==='split-clip'&&clip){const duration=Math.max(.001,Number(clip.duration)||.001),half=duration/2,second={...clone(clip),id:text(clip.id)+'-split-'+String(revision+1),start:(Number(clip.start)||0)+half,duration:half,trimStart:(Number(clip.trimStart)||0)+half};clip.duration=half;draft.clips=[...draft.clips.slice(0,index+1),second,...draft.clips.slice(index+1)];changed();draw();return}if(kind==='remove-clip'){draft.clips=draft.clips.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-audio'){draft.audioItems=draft.audioItems.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-text'){draft.textOverlays=draft.textOverlays.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-caption'){draft.captions=draft.captions.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-effect'){draft.effects=draft.effects.filter((item,position)=>position!==index);changed();draw();return}\n if(clip){if(kind==='clip-track')clip.trackId=event.value;if(kind==='clip-start')clip.start=Math.max(0,Number(event.value)||0);if(kind==='clip-duration')clip.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='clip-trim')clip.trimStart=Math.max(0,Number(event.value)||0);if(kind==='clip-speed')clip.speed=Math.max(0.01,Number(event.value)||1);if(kind==='clip-volume')clip.volume=Math.max(0,Number(event.value)||0);if(kind==='clip-transition'||kind==='clip-transition-duration'){if(!clip.transition||Array.isArray(clip.transition))clip.transition={type:'Cut',duration:0};if(kind==='clip-transition')clip.transition.type=event.value;else clip.transition.duration=Math.max(0,Number(event.value)||0)}if(kind==='clip-opacity'){if(!clip.transform||Array.isArray(clip.transform))clip.transform={};clip.transform.opacity=Math.max(0,Math.min(1,Number(event.value)||0))}if(kind==='clip-reverse')clip.reverse=event.checked;if(kind==='clip-motion'||kind==='clip-easing'){if(!clip.motion||Array.isArray(clip.motion))clip.motion={type:'none',easing:'ease-in-out'};if(kind==='clip-motion')clip.motion.type=event.value;else clip.motion.easing=event.value}}\n if(audio){if(kind==='audio-track')audio.trackId=event.value;if(kind==='audio-start')audio.start=Math.max(0,Number(event.value)||0);if(kind==='audio-duration')audio.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='audio-trim'){audio.trimStart=Math.max(0,Number(event.value)||0);audio.sourceOffset=audio.trimStart}if(kind==='audio-volume')audio.volume=Math.max(0,Number(event.value)||0);if(kind==='audio-fade-in')audio.fadeIn=Math.max(0,Number(event.value)||0);if(kind==='audio-fade-out')audio.fadeOut=Math.max(0,Number(event.value)||0);if(kind==='audio-ducking')audio.ducking=Math.max(0,Math.min(1,Number(event.value)||0));if(kind==='audio-rate')audio.rate=Math.max(.05,Math.min(4,Number(event.value)||1));if(kind==='audio-loop')audio.loop=event.checked;if(kind==='audio-muted')audio.muted=event.checked}\n if(title){if(kind==='text-value')title.text=event.value;if(kind==='text-track')title.trackId=event.value;if(kind==='text-start')title.start=Math.max(0,Number(event.value)||0);if(kind==='text-duration')title.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='text-animation')title.animation=event.value}\n if(caption){if(kind==='caption-value')caption.text=event.value;if(kind==='caption-track')caption.trackId=event.value;if(kind==='caption-start')caption.start=Math.max(0,Number(event.value)||0);if(kind==='caption-end')caption.end=Math.max(caption.start+0.001,Number(event.value)||caption.start+0.001);if(kind==='caption-animation')caption.animation=event.value}\n if(effect){if(kind==='effect-type')effect.type=event.value;if(kind==='effect-track')effect.trackId=event.value;if(kind==='effect-start')effect.start=Math.max(0,Number(event.value)||0);if(kind==='effect-duration')effect.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='effect-intensity')effect.intensity=Number(event.value)}changed()});\nbuild.addEventListener('click',()=>{if(!busy){draft=assemble();changed();draw();status.textContent='Built an editable timeline from current project media.'}});\ndocument.getElementById('addText').addEventListener('click',()=>{if(busy)return;ensure();draft.textOverlays=[...draft.textOverlays,{id:'portable-text-'+String(draft.textOverlays.length+1)+'-'+String(revision+1),trackId:'text',text:'New text',start:0,duration:2,animation:'none'}];changed();draw()});\ndocument.getElementById('addCaption').addEventListener('click',()=>{if(busy)return;ensure();draft.captions=[...draft.captions,{id:'portable-caption-'+String(draft.captions.length+1)+'-'+String(revision+1),trackId:'captions',text:'New caption',start:0,end:2,font:'Hanken Grotesk',animation:'none'}];changed();draw()});\ndocument.getElementById('addEffect').addEventListener('click',()=>{if(busy)return;ensure();draft.effects=[...draft.effects,{id:'portable-effect-'+String(draft.effects.length+1)+'-'+String(revision+1),trackId:'video',type:'fade',start:0,duration:1,intensity:1}];changed();draw()});\nplayhead.addEventListener('input',composition);\ndocument.getElementById('undo').addEventListener('click',()=>{if(busy||editStackIndex<=0)return;editStackIndex-=1;draft=clone(editStack.find((item,index)=>index===editStackIndex));revision+=1;draw();status.textContent='Undid the latest local change.'});\ndocument.getElementById('redo').addEventListener('click',()=>{if(busy||editStackIndex>=editStack.length-1)return;editStackIndex+=1;draft=clone(editStack.find((item,index)=>index===editStackIndex));revision+=1;draw();status.textContent='Restored the next local change.'});\ndocument.getElementById('snapshot').addEventListener('click',()=>{if(busy)return;ensure();const id='timeline-version-'+String(Date.now())+'-'+String(revision+1),version={id,label:'Timeline version '+String((draft.timelineVersions||[]).length+1),createdAt:new Date().toISOString(),clips:clone(draft.clips),audioItems:clone(draft.audioItems),captions:clone(draft.captions),textOverlays:clone(draft.textOverlays),effects:clone(draft.effects)};draft.timelineVersions=[...(draft.timelineVersions||[]),version].slice(-50);changed();draw();status.textContent='Saved a restorable timeline version in this Editor document.'});\ndocument.getElementById('applyRaw').addEventListener('click',()=>{if(busy)return;try{const value=JSON.parse(raw.value);if(!value||Array.isArray(value)||value.schemaVersion!==1||typeof value.activeTimelineId!=='string')throw Error('Editor JSON must be a version 1 document with an active timeline ID.');draft=clone(value);changed();draw();status.textContent='Complete Editor data applied locally.'}catch(error){status.textContent=error.message}});\ndocument.getElementById('reset').addEventListener('click',()=>{if(!busy){draft=clone(source);revision+=1;draw();status.textContent='Changes reset to the current Editor timeline.'}});\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Editor timeline…';try{const result=await StillMade.run({editor:source,updatedEditor:draft,commit:true});draft=clone(result.editor);if(started===revision)status.textContent='Editor changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'Editor changes could not be prepared.'}finally{busy=false;draw()}});\nStillMade.onInput(values=>{source=clone(values.editor||source);assets=clone(values.assets||[]);draft=values.updatedEditor&&(values.updatedEditor.clips?.length||values.updatedEditor.audioItems?.length||values.updatedEditor.timelineVersions?.length)?clone(values.updatedEditor):source.clips?.length||source.audioItems?.length?clone(source):assemble();revision+=1;editStack=[clone(draft)];editStackIndex=0;playhead.value='0';draw();status.textContent='Edit the complete timeline. Motion graphics and every other Editor tool stay inside this Block.'});\n\ndocument.getElementById('renderFinal').addEventListener('click',async()=>{if(busy)return;busy=true;draw();status.textContent='Preparing the exact final render…';try{await StillMade.run({editor:source,updatedEditor:draft,commit:false,action:'render',renderFps:Number(document.getElementById('renderFps').value),renderResolution:Number(document.getElementById('renderResolution').value),renderQuality:document.getElementById('renderQuality').value});status.textContent='Final render is ready below. Choose Render final file to start the device renderer.'}catch(error){status.textContent=error.message||'The render plan could not be prepared.'}finally{busy=false;draw()}});\n"},"tests":[{"name":"Retain the complete Editor and all internal tools","input":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","aspect":"16:9","tracks":[{"id":"video","type":"video"},{"id":"audio","type":"audio"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","motion":{"type":"push-in"},"color":{"brightness":5},"transition":{"type":"Fade","duration":0.5}}],"audioItems":[{"id":"music-1","trackId":"audio","start":0,"duration":4,"url":"https://media.example/music.mp3","volume":70}],"captions":[{"id":"caption-1","trackId":"captions","text":"Hello","start":0,"end":1}],"textOverlays":[{"id":"title-1","text":"Opening","start":0,"duration":2}],"effects":[{"id":"effect-1","type":"grain","start":0,"duration":4}],"fullscreenFrames":[{"id":"mg-frame-1","title":"Motion graphic"}],"figureSweeps":[{"id":"mg-figure-1","value":42}],"audioScore":{"wholeMusic":{"versions":[{"url":"https://media.example/score.mp3"}],"idx":0}},"timelineVersions":[{"id":"timeline-1","name":"Main","clips":[]}],"exportSettings":{"resolution":"1080p"},"custom":{"keep":true}},"updatedEditor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"commit":false},"expected":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","aspect":"16:9","tracks":[{"id":"video","type":"video"},{"id":"audio","type":"audio"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","motion":{"type":"push-in"},"color":{"brightness":5},"transition":{"type":"Fade","duration":0.5}}],"audioItems":[{"id":"music-1","trackId":"audio","start":0,"duration":4,"url":"https://media.example/music.mp3","volume":70}],"captions":[{"id":"caption-1","trackId":"captions","text":"Hello","start":0,"end":1}],"textOverlays":[{"id":"title-1","text":"Opening","start":0,"duration":2}],"effects":[{"id":"effect-1","type":"grain","start":0,"duration":4}],"fullscreenFrames":[{"id":"mg-frame-1","title":"Motion graphic"}],"figureSweeps":[{"id":"mg-figure-1","value":42}],"audioScore":{"wholeMusic":{"versions":[{"url":"https://media.example/score.mp3"}],"idx":0}},"timelineVersions":[{"id":"timeline-1","name":"Main","clips":[]}],"exportSettings":{"resolution":"1080p"},"custom":{"keep":true}},"edit":{"schemaVersion":1,"title":"Update Editor timeline timeline-1","field":"editor","before":{"schemaVersion":1,"activeTimelineId":"timeline-1","aspect":"16:9","tracks":[{"id":"video","type":"video"},{"id":"audio","type":"audio"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","motion":{"type":"push-in"},"color":{"brightness":5},"transition":{"type":"Fade","duration":0.5}}],"audioItems":[{"id":"music-1","trackId":"audio","start":0,"duration":4,"url":"https://media.example/music.mp3","volume":70}],"captions":[{"id":"caption-1","trackId":"captions","text":"Hello","start":0,"end":1}],"textOverlays":[{"id":"title-1","text":"Opening","start":0,"duration":2}],"effects":[{"id":"effect-1","type":"grain","start":0,"duration":4}],"fullscreenFrames":[{"id":"mg-frame-1","title":"Motion graphic"}],"figureSweeps":[{"id":"mg-figure-1","value":42}],"audioScore":{"wholeMusic":{"versions":[{"url":"https://media.example/score.mp3"}],"idx":0}},"timelineVersions":[{"id":"timeline-1","name":"Main","clips":[]}],"exportSettings":{"resolution":"1080p"},"custom":{"keep":true}},"after":{"schemaVersion":1,"activeTimelineId":"timeline-1","aspect":"16:9","tracks":[{"id":"video","type":"video"},{"id":"audio","type":"audio"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","motion":{"type":"push-in"},"color":{"brightness":5},"transition":{"type":"Fade","duration":0.5}}],"audioItems":[{"id":"music-1","trackId":"audio","start":0,"duration":4,"url":"https://media.example/music.mp3","volume":70}],"captions":[{"id":"caption-1","trackId":"captions","text":"Hello","start":0,"end":1}],"textOverlays":[{"id":"title-1","text":"Opening","start":0,"duration":2}],"effects":[{"id":"effect-1","type":"grain","start":0,"duration":4}],"fullscreenFrames":[{"id":"mg-frame-1","title":"Motion graphic"}],"figureSweeps":[{"id":"mg-figure-1","value":42}],"audioScore":{"wholeMusic":{"versions":[{"url":"https://media.example/score.mp3"}],"idx":0}},"timelineVersions":[{"id":"timeline-1","name":"Main","clips":[]}],"exportSettings":{"resolution":"1080p"},"custom":{"keep":true}}}}},{"name":"Prepare one exact complete Editor update","input":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","type":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","volume":100}],"audioItems":[],"captions":[],"textOverlays":[{"id":"title-1","text":"Old","start":0,"duration":2}],"effects":[]},"updatedEditor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","type":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":1,"duration":3,"url":"https://media.example/clip.mp4","volume":80}],"audioItems":[],"captions":[],"textOverlays":[{"id":"title-1","text":"New","start":1,"duration":2}],"effects":[]},"commit":true},"expected":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","type":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":1,"duration":3,"url":"https://media.example/clip.mp4","volume":80}],"audioItems":[],"captions":[],"textOverlays":[{"id":"title-1","text":"New","start":1,"duration":2}],"effects":[]},"edit":{"schemaVersion":1,"title":"Update Editor timeline timeline-1","field":"editor","before":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","type":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":0,"duration":4,"url":"https://media.example/clip.mp4","volume":100}],"audioItems":[],"captions":[],"textOverlays":[{"id":"title-1","text":"Old","start":0,"duration":2}],"effects":[]},"after":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","type":"video"}],"clips":[{"id":"clip-1","trackId":"video","start":1,"duration":3,"url":"https://media.example/clip.mp4","volume":80}],"audioItems":[],"captions":[],"textOverlays":[{"id":"title-1","text":"New","start":1,"duration":2}],"effects":[]}}}},{"name":"Build initial timeline from exact project media versions","input":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"assets":[{"kind":"video","assetId":"video-1","versionId":"v1","url":"https://media.example/video.mp4","duration":5},{"kind":"image","assetId":"image-1","versionId":"v2","url":"https://media.example/image.png"},{"kind":"audio","assetId":"music-1","versionId":"v1","url":"https://media.example/music.mp3","duration":9}],"updatedEditor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"commit":false,"action":"none"},"expected":{"editor":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","kind":"video","label":"Video"},{"id":"audio","kind":"audio","label":"Audio"}],"clips":[{"id":"clip-1-video-1","trackId":"video","start":0,"duration":5,"trimStart":0,"speed":1,"volume":1,"asset":{"kind":"video","assetId":"video-1","versionId":"v1","url":"https://media.example/video.mp4","duration":5},"url":"https://media.example/video.mp4"},{"id":"clip-2-image-1","trackId":"video","start":5,"duration":4,"trimStart":0,"speed":1,"volume":1,"asset":{"kind":"image","assetId":"image-1","versionId":"v2","url":"https://media.example/image.png"},"url":"https://media.example/image.png"}],"audioItems":[{"id":"audio-1-music-1","trackId":"audio","start":0,"duration":9,"trimStart":0,"volume":1,"asset":{"kind":"audio","assetId":"music-1","versionId":"v1","url":"https://media.example/music.mp3","duration":9},"url":"https://media.example/music.mp3"}],"captions":[],"textOverlays":[],"effects":[],"targetDuration":9},"edit":{"schemaVersion":1,"title":"Update Editor timeline timeline-1","field":"editor","before":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[],"clips":[],"audioItems":[],"captions":[],"textOverlays":[],"effects":[]},"after":{"schemaVersion":1,"activeTimelineId":"timeline-1","tracks":[{"id":"video","kind":"video","label":"Video"},{"id":"audio","kind":"audio","label":"Audio"}],"clips":[{"id":"clip-1-video-1","trackId":"video","start":0,"duration":5,"trimStart":0,"speed":1,"volume":1,"asset":{"kind":"video","assetId":"video-1","versionId":"v1","url":"https://media.example/video.mp4","duration":5},"url":"https://media.example/video.mp4"},{"id":"clip-2-image-1","trackId":"video","start":5,"duration":4,"trimStart":0,"speed":1,"volume":1,"asset":{"kind":"image","assetId":"image-1","versionId":"v2","url":"https://media.example/image.png"},"url":"https://media.example/image.png"}],"audioItems":[{"id":"audio-1-music-1","trackId":"audio","start":0,"duration":9,"trimStart":0,"volume":1,"asset":{"kind":"audio","assetId":"music-1","versionId":"v1","url":"https://media.example/music.mp3","duration":9},"url":"https://media.example/music.mp3"}],"captions":[],"textOverlays":[],"effects":[],"targetDuration":9}}}}],"portable":{"format":"stillmade-block/1","dependencies":[],"sources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}],"files":{"upstream/0/LICENSE":"MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","upstream/0/src/run.js":"const copy = value => JSON.parse(JSON.stringify(value));\nconst source = copy(input.editor || {schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]});\nconst assets=Array.isArray(input.assets)?input.assets.filter(asset=>asset&&['image','video','audio'].includes(asset.kind)&&asset.assetId&&asset.versionId&&asset.url).slice(0,500):[];\nconst assembled=copy(source);if(!assembled.clips?.length&&!assembled.audioItems?.length&&assets.length){assembled.tracks=[{id:'video',kind:'video',label:'Video'},{id:'audio',kind:'audio',label:'Audio'}];let cursor=0;assembled.clips=assets.filter(asset=>asset.kind!=='audio').map((asset,index)=>{const duration=Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||4),clip={id:'clip-'+String(index+1)+'-'+String(asset.assetId),trackId:'video',start:cursor,duration,trimStart:0,speed:1,volume:1,asset:copy(asset),url:asset.url};cursor+=duration;return clip});assembled.audioItems=assets.filter(asset=>asset.kind==='audio').map((asset,index)=>({id:'audio-'+String(index+1)+'-'+String(asset.assetId),trackId:'audio',start:0,duration:Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||cursor||1),trimStart:0,volume:1,asset:copy(asset),url:asset.url}));assembled.targetDuration=Math.max(cursor,...assembled.audioItems.map(item=>item.duration));}\nconst candidate = input.commit || input.action === 'render' ? copy(input.updatedEditor) : assembled;\nif (!candidate || Array.isArray(candidate) || candidate.schemaVersion !== 1 || typeof candidate.activeTimelineId !== 'string' || !candidate.activeTimelineId) throw new Error('Keep the complete version 1 Editor document and active timeline identity.');\nconst names=['tracks','clips','audioItems','captions','textOverlays','stickers','effects','markers','fxEvents','liveOverlays','mediaItems','timelineVersions'];\nif (!names.every(name => candidate[name] === undefined || Array.isArray(candidate[name]) && candidate[name].length <= 10000 && candidate[name].every(item => item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string' && item.id.length > 0 && item.id.length <= 200) && new Set(candidate[name].map(item => item.id)).size === candidate[name].length)) throw new Error('Editor collections need valid distinct retained item IDs.');\nconst itemEnd=item=>Math.max(0,Number(item.start)||0)+Math.max(.001,Number(item.duration)||Number(item.durationMs)/1000||.001);\nconst outputs={editor:candidate,edit:{schemaVersion:1,title:`Update Editor timeline ${candidate.activeTimelineId}`,field:'editor',before:source,after:candidate}};\nif(input.action==='render'){const clips=Array.isArray(candidate.clips)?candidate.clips:[],audio=Array.isArray(candidate.audioItems)?candidate.audioItems:[],overlays=[...(candidate.captions||[]),...(candidate.textOverlays||[]),...(candidate.effects||[])],targetDuration=Math.max(.001,Number(candidate.targetDuration)||0,...clips.map(itemEnd),...audio.map(itemEnd),...overlays.map(itemEnd));outputs.mediaAction={schemaVersion:1,kind:'stillmade.media-action',operation:'final.render',request:{schemaVersion:1,kind:'video',timeline:{...candidate,clips,audio,overlays,targetDuration},settings:{fps:input.renderFps,resolution:input.renderResolution,quality:input.renderQuality},source:{id:'sm.editor',version:'2.2.6'}},resultPort:null};}\nreturn outputs;\n","upstream/0/src/view.json":"{\n  \"html\": \"<main class=\\\"editor-workspace\\\">\\n  <header><div><small>Production workspace</small><h1>Editor</h1><p id=\\\"summary\\\">Loading the complete timeline…</p></div><div class=\\\"actions\\\"><button id=\\\"build\\\" type=\\\"button\\\" class=\\\"secondary\\\">Build from project media</button><button id=\\\"undo\\\" type=\\\"button\\\" class=\\\"secondary\\\">Undo</button><button id=\\\"redo\\\" type=\\\"button\\\" class=\\\"secondary\\\">Redo</button><button id=\\\"snapshot\\\" type=\\\"button\\\" class=\\\"secondary\\\">Save version</button><button id=\\\"reset\\\" type=\\\"button\\\" class=\\\"secondary\\\">Reset changes</button><button id=\\\"addText\\\" type=\\\"button\\\" class=\\\"secondary\\\">Add text</button><button id=\\\"addCaption\\\" type=\\\"button\\\" class=\\\"secondary\\\">Add caption</button><button id=\\\"save\\\" type=\\\"button\\\" data-stillmade-action=\\\"run\\\">Review changes</button></div></header>\\n  <output id=\\\"status\\\" role=\\\"status\\\" aria-live=\\\"polite\\\"></output>\\n  <section><h2>Timeline</h2><div class=\\\"grid\\\"><label>Active timeline<input id=\\\"timelineId\\\" type=\\\"text\\\" disabled></label><label>Aspect ratio<input id=\\\"aspect\\\" type=\\\"text\\\"></label><label>Playhead (seconds)<input id=\\\"playhead\\\" type=\\\"range\\\" min=\\\"0\\\" max=\\\"1\\\" step=\\\"0.05\\\" value=\\\"0\\\"></label></div><p id=\\\"timecode\\\" class=\\\"hint\\\"></p><div id=\\\"composition\\\" class=\\\"cards\\\"></div></section>\\n  <section><h2>Project media</h2><p class=\\\"hint\\\">Preview an exact saved version, then add it to this timeline without leaving the Block.</p><img id=\\\"imagePreview\\\" alt=\\\"Selected project media preview\\\" hidden><video id=\\\"videoPreview\\\" controls hidden></video><audio id=\\\"audioPreview\\\" controls hidden></audio><div id=\\\"assets\\\" class=\\\"cards media-grid\\\"></div></section>\\n  <section><h2>Video</h2><div id=\\\"clips\\\" class=\\\"cards\\\"></div></section>\\n  <section><h2>Audio and scoring</h2><div id=\\\"audio\\\" class=\\\"cards\\\"></div></section>\\n  <section><h2>Saved timeline versions</h2><div id=\\\"versions\\\" class=\\\"cards\\\"></div></section>\\n  <section><h2>Text and motion graphics</h2><div id=\\\"text\\\" class=\\\"cards\\\"></div><p id=\\\"graphics\\\" class=\\\"hint\\\"></p></section>\\n  <section><h2>Captions</h2><div id=\\\"captions\\\" class=\\\"cards\\\"></div></section>\\n  <section><div class=\\\"card-head\\\"><h2>Effects</h2><button id=\\\"addEffect\\\" type=\\\"button\\\" class=\\\"secondary\\\">Add effect</button></div><div id=\\\"effects\\\" class=\\\"cards\\\"></div></section>\\n  <details><summary>Complete Editor data</summary><p>All timeline lanes, motion graphics, grade settings, transitions, scoring, versions, and export settings are retained. Use this editor for fields not shown on the lane cards.</p><textarea id=\\\"raw\\\" class=\\\"raw\\\" spellcheck=\\\"false\\\" aria-label=\\\"Complete Editor data\\\"></textarea><button id=\\\"applyRaw\\\" type=\\\"button\\\" class=\\\"secondary\\\">Apply JSON to workspace</button></details>\\n<section><h2>Final export</h2><p>Render this exact timeline through StillMade’s approved device renderer.</p><div class=\\\"grid\\\"><label>Frame rate<select id=\\\"renderFps\\\"><option value=\\\"24\\\">24 fps</option><option value=\\\"30\\\" selected>30 fps</option><option value=\\\"60\\\">60 fps</option></select></label><label>Resolution<select id=\\\"renderResolution\\\"><option value=\\\"720\\\">720p</option><option value=\\\"1080\\\" selected>1080p</option><option value=\\\"2160\\\">2160p</option></select></label><label>Quality<select id=\\\"renderQuality\\\"><option value=\\\"standard\\\">Standard</option><option value=\\\"high\\\" selected>High</option></select></label></div><button id=\\\"renderFinal\\\" type=\\\"button\\\">Render final video</button></section></main>\\n\",\n  \"css\": \".editor-workspace{max-width:1160px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head,.row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}header{justify-content:space-between;margin-bottom:14px}header h1{margin:2px 0}header p,header small,details p,output,.hint{color:var(--fg-muted)}section,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}section h2{margin:0 0 10px}.cards{display:grid;gap:10px}.card{padding:12px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.card-head{justify-content:space-between}.card h3{margin:0;font-size:16px}.grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}label{display:grid;gap:5px;margin:7px 0;font-size:13px;font-weight:600}input,textarea{width:100%;background:var(--bg)}textarea{min-height:70px;resize:vertical}.wide{grid-column:1/-1}.secondary{background:transparent;color:var(--fg)}.danger{background:transparent;color:var(--signal)}.raw{min-height:440px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}\\n@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.editor-workspace{padding:0}.grid{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}section,details{padding:12px}}\\n.media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.media-card p{margin:4px 0;color:var(--fg-muted)}video,audio{display:block;width:100%;margin:10px 0}video[hidden],audio[hidden]{display:none}select{width:100%;background:var(--bg)}\\n\",\n  \"javascript\": \"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save'),aspect=document.getElementById('aspect'),timelineId=document.getElementById('timelineId'),graphics=document.getElementById('graphics'),build=document.getElementById('build'),imagePreview=document.getElementById('imagePreview'),videoPreview=document.getElementById('videoPreview'),audioPreview=document.getElementById('audioPreview'),playhead=document.getElementById('playhead'),timecode=document.getElementById('timecode');\\nlet source={schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]},draft={schemaVersion:1,activeTimelineId:'timeline-1',tracks:[],clips:[],audioItems:[],captions:[],textOverlays:[],effects:[]},assets=[],previewUrl='',busy=false,revision=0,editStack=[],editStackIndex=-1;\\nconst clone=value=>JSON.parse(JSON.stringify(value));\\nconst text=value=>value===undefined||value===null?'':String(value);\\nconst assemble=()=>{const next=clone(source),media=assets.filter(asset=>asset&&['image','video','audio'].includes(asset.kind)&&asset.assetId&&asset.versionId&&asset.url).slice(0,500);if(next.clips?.length||next.audioItems?.length||!media.length)return next;next.tracks=[{id:'video',kind:'video',label:'Video'},{id:'audio',kind:'audio',label:'Audio'}];let cursor=0;next.clips=media.filter(asset=>asset.kind!=='audio').map((asset,index)=>{const duration=Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||4),clip={id:'clip-'+String(index+1)+'-'+text(asset.assetId),trackId:'video',start:cursor,duration,trimStart:0,speed:1,volume:1,asset:clone(asset),url:asset.url};cursor+=duration;return clip});next.audioItems=media.filter(asset=>asset.kind==='audio').map((asset,index)=>({id:'audio-'+String(index+1)+'-'+text(asset.assetId),trackId:'audio',start:0,duration:Math.max(.001,Number(asset.duration)||Number(asset.durationSec)||cursor||1),trimStart:0,volume:1,asset:clone(asset),url:asset.url}));next.targetDuration=Math.max(cursor,...next.audioItems.map(item=>item.duration));return next};\\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\\nconst remove=action=>({tag:'button',action,text:'Remove',className:'danger',disabled:busy});\\nconst assetCard=(item,index)=>({tag:'article',className:'card media-card',children:[{tag:'strong',text:text(item.name)||text(item.assetId)},{tag:'p',text:text(item.kind)+' · version '+text(item.versionId)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-asset:'+String(index),text:'Preview',className:'secondary',disabled:busy},{tag:'button',action:'add-asset:'+String(index),text:item.kind==='audio'?'Add audio':'Add clip',className:'secondary',disabled:busy}]}]});\\nconst clipCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.label)||text(item.name)||text(item.id)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-clip:'+String(index),text:'Preview',className:'secondary',disabled:busy||!(item.asset||item.assetId)},{tag:'button',action:'move-clip-up:'+String(index),text:'Up',className:'secondary',disabled:busy||index===0},{tag:'button',action:'move-clip-down:'+String(index),text:'Down',className:'secondary',disabled:busy||index===draft.clips.length-1},{tag:'button',action:'split-clip:'+String(index),text:'Split',className:'secondary',disabled:busy||Number(item.duration)<=0.002},{tag:'button',action:'duplicate-clip:'+String(index),text:'Duplicate',className:'secondary',disabled:busy},remove('remove-clip:'+String(index))]}]},{tag:'div',className:'grid',children:[field('Track','clip-track:'+String(index),item.trackId),field('Start (seconds)','clip-start:'+String(index),item.start,'number'),field('Length (seconds)','clip-duration:'+String(index),item.duration,'number'),field('Trim start','clip-trim:'+String(index),item.trimStart,'number'),field('Speed','clip-speed:'+String(index),item.speed,'number'),field('Volume','clip-volume:'+String(index),item.volume,'number'),field('Transition','clip-transition:'+String(index),item.transition&&item.transition.type),field('Transition length','clip-transition-duration:'+String(index),item.transition&&item.transition.duration,'number'),field('Motion','clip-motion:'+String(index),item.motion&&item.motion.type),field('Motion easing','clip-easing:'+String(index),item.motion&&item.motion.easing),field('Opacity','clip-opacity:'+String(index),item.transform&&item.transform.opacity===undefined?item.opacity:item.transform.opacity,'number'),{tag:'label',children:[{tag:'span',text:'Reverse playback'},{tag:'input',type:'checkbox',action:'clip-reverse:'+String(index),checked:!!item.reverse,disabled:busy,label:'Reverse playback'}]}]}]});\\nconst audioCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.label)||text(item.name)||text(item.id)},{tag:'div',className:'row',children:[{tag:'button',action:'preview-audio:'+String(index),text:'Preview',className:'secondary',disabled:busy||!(item.asset||item.assetId)},remove('remove-audio:'+String(index))]}]},{tag:'div',className:'grid',children:[field('Track','audio-track:'+String(index),item.trackId),field('Start (seconds)','audio-start:'+String(index),item.start,'number'),field('Length (seconds)','audio-duration:'+String(index),item.duration,'number'),field('Trim start','audio-trim:'+String(index),item.trimStart===undefined?item.sourceOffset:item.trimStart,'number'),field('Volume','audio-volume:'+String(index),item.volume,'number'),field('Fade in','audio-fade-in:'+String(index),item.fadeIn,'number'),field('Fade out','audio-fade-out:'+String(index),item.fadeOut,'number'),field('Ducking','audio-ducking:'+String(index),item.ducking,'number'),field('Playback speed','audio-rate:'+String(index),item.rate,'number'),{tag:'label',children:[{tag:'span',text:'Loop source'},{tag:'input',type:'checkbox',action:'audio-loop:'+String(index),checked:!!item.loop,disabled:busy,label:'Loop source'}]},{tag:'label',children:[{tag:'span',text:'Muted'},{tag:'input',type:'checkbox',action:'audio-muted:'+String(index),checked:!!item.muted,disabled:busy,label:'Muted'}]}]}]});\\nconst textCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.id)},remove('remove-text:'+String(index))]},{tag:'div',className:'grid',children:[field('Text','text-value:'+String(index),item.text,'text',true),field('Track','text-track:'+String(index),item.trackId),field('Start (seconds)','text-start:'+String(index),item.start,'number'),field('Length (seconds)','text-duration:'+String(index),item.duration,'number'),field('Animation','text-animation:'+String(index),item.animation)]}]});\\nconst captionCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.id)},remove('remove-caption:'+String(index))]},{tag:'div',className:'grid',children:[field('Caption','caption-value:'+String(index),item.text,'text',true),field('Track','caption-track:'+String(index),item.trackId),field('Start (seconds)','caption-start:'+String(index),item.start,'number'),field('End (seconds)','caption-end:'+String(index),item.end,'number'),field('Animation','caption-animation:'+String(index),item.animation)]}]});\\nconst effectCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(item.type)||text(item.id)},remove('remove-effect:'+String(index))]},{tag:'div',className:'grid',children:[field('Type','effect-type:'+String(index),item.type),field('Track','effect-track:'+String(index),item.trackId),field('Start (seconds)','effect-start:'+String(index),item.start,'number'),field('Length (seconds)','effect-duration:'+String(index),item.duration,'number'),field('Intensity','effect-intensity:'+String(index),item.intensity,'number')]}]});\\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft=clone(source);if(!Array.isArray(draft.tracks))draft.tracks=[];if(!Array.isArray(draft.clips))draft.clips=[];if(!Array.isArray(draft.audioItems))draft.audioItems=[];if(!Array.isArray(draft.captions))draft.captions=[];if(!Array.isArray(draft.textOverlays))draft.textOverlays=[];if(!Array.isArray(draft.effects))draft.effects=[]};\\nconst itemFinish=item=>(Number(item.start)||0)+Math.max(.001,Number(item.duration)||Number(item.end)-(Number(item.start)||0)||.001);const composition=()=>{const at=Number(playhead.value)||0,activeClips=draft.clips.filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item)),activeAudio=draft.audioItems.filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item)),activeOverlays=[...draft.textOverlays,...draft.captions,...draft.effects].filter(item=>at>=(Number(item.start)||0)&&at<itemFinish(item));StillMade.render('composition',[{tag:'strong',text:'At '+at.toFixed(2)+'s'},{tag:'p',text:(activeClips.length?activeClips.map(item=>text(item.label)||text(item.id)).join(', '):'No video')+' · '+String(activeAudio.length)+' audio · '+String(activeOverlays.length)+' overlays'}]);timecode.textContent='Playhead '+at.toFixed(2)+' seconds'};const versionCard=(item,index)=>({tag:'article',className:'card',children:[{tag:'strong',text:text(item.label)||text(item.id)},{tag:'p',text:text(item.createdAt)},{tag:'button',action:'restore-version:'+String(index),text:'Restore this version',className:'secondary',disabled:busy}]});const draw=()=>{ensure();StillMade.render('assets',assets.length?assets.map(assetCard):[{tag:'p',text:'No saved project media is available.'}]);StillMade.render('clips',draft.clips.length?draft.clips.map(clipCard):[{tag:'p',text:'No video clips.'}]);StillMade.render('audio',draft.audioItems.length?draft.audioItems.map(audioCard):[{tag:'p',text:'No audio items.'}]);StillMade.render('text',draft.textOverlays.length?draft.textOverlays.map(textCard):[{tag:'p',text:'No text overlays.'}]);StillMade.render('captions',draft.captions.length?draft.captions.map(captionCard):[{tag:'p',text:'No captions.'}]);StillMade.render('versions',Array.isArray(draft.timelineVersions)&&draft.timelineVersions.length?draft.timelineVersions.map(versionCard):[{tag:'p',text:'No saved versions yet.'}]);StillMade.render('effects',draft.effects.length?draft.effects.map(effectCard):[{tag:'p',text:'No effects.'}]);timelineId.value=text(draft.activeTimelineId);const duration=Math.max(1,Number(draft.targetDuration)||0,...draft.clips.map(itemFinish),...draft.audioItems.map(itemFinish));playhead.max=String(duration);composition();aspect.value=text(draft.aspect);const mg=(Array.isArray(draft.fullscreenFrames)?draft.fullscreenFrames.length:0)+(Array.isArray(draft.figureSweeps)?draft.figureSweeps.length:0)+(Array.isArray(draft.graphs)?draft.graphs.length:0);graphics.textContent=String(mg)+' motion graphic item'+(mg===1?'':'s')+' retained in this Editor Block. Use Complete Editor data for advanced motion-graphics fields.';raw.value=JSON.stringify(draft,null,2);summary.textContent=String(draft.clips.length)+' video · '+String(draft.audioItems.length)+' audio · '+String(draft.captions.length)+' captions · '+String(draft.textOverlays.length)+' text';save.disabled=busy;build.disabled=busy||!assets.length;document.getElementById('reset').disabled=busy;document.getElementById('addText').disabled=busy;document.getElementById('addCaption').disabled=busy;document.getElementById('addEffect').disabled=busy;document.getElementById('undo').disabled=busy||editStackIndex<=0;document.getElementById('redo').disabled=busy||editStackIndex>=editStack.length-1;document.getElementById('snapshot').disabled=busy};\\nconst changed=()=>{revision+=1;editStack=editStack.slice(0,editStackIndex+1);editStack=[...editStack,clone(draft)].slice(-50);editStackIndex=editStack.length-1;raw.value=JSON.stringify(draft,null,2);status.textContent='Changes are local until you choose Review changes.'};\\naspect.addEventListener('input',()=>{draft.aspect=aspect.value;changed()});\\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],index=Number(parts[1]),clip=draft.clips.find((item,position)=>position===index),audio=draft.audioItems.find((item,position)=>position===index),title=draft.textOverlays.find((item,position)=>position===index),caption=draft.captions.find((item,position)=>position===index),effect=draft.effects.find((item,position)=>position===index),asset=assets.find((item,position)=>position===index);\\n if(kind==='preview-clip'&&clip){const value=clip.asset||clip;if(previewUrl)StillMade.releaseMedia(previewUrl);const request=value.kind==='image'?StillMade.previewImage(value):StillMade.previewMedia({...value,kind:value.kind||'video'});request.then(preview=>{imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.hidden=true;if(value.kind==='image'){imagePreview.src=preview.url;imagePreview.hidden=false}else{previewUrl=preview.url;videoPreview.src=preview.url;videoPreview.hidden=false;videoPreview.currentTime=Math.max(0,Number(clip.trimStart)||0)}status.textContent='Previewing '+(clip.label||clip.name||clip.id)+'.'}).catch(error=>{status.textContent=error.message});return} if(kind==='preview-audio'&&audio){const value=audio.asset||audio;if(previewUrl)StillMade.releaseMedia(previewUrl);StillMade.previewMedia(value).then(preview=>{previewUrl=preview.url;imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.src=preview.url;audioPreview.hidden=false;status.textContent='Previewing '+(audio.label||audio.name||audio.id)+'.'}).catch(error=>{status.textContent=error.message});return}if(kind==='restore-version'&&Array.isArray(draft.timelineVersions)){const version=draft.timelineVersions.find((item,position)=>position===index);if(version){draft={...draft,clips:clone(version.clips||[]),audioItems:clone(version.audioItems||[]),captions:clone(version.captions||[]),textOverlays:clone(version.textOverlays||[]),effects:clone(version.effects||[])};changed();draw();status.textContent='Saved timeline version restored locally.'}return}if(kind==='preview-asset'&&asset){if(previewUrl)StillMade.releaseMedia(previewUrl);imagePreview.hidden=true;videoPreview.hidden=true;audioPreview.hidden=true;const request=asset.kind==='image'?StillMade.previewImage(asset):StillMade.previewMedia(asset);request.then(value=>{previewUrl=asset.kind==='image'?'':value.url;if(asset.kind==='image'){imagePreview.src=value.url;imagePreview.hidden=false}else if(asset.kind==='video'){videoPreview.src=value.url;videoPreview.hidden=false}else{audioPreview.src=value.url;audioPreview.hidden=false}status.textContent='Previewing '+(asset.name||asset.assetId)+'.'}).catch(error=>{status.textContent=error.message});return}if(kind==='add-asset'&&asset){const media=clone(asset),duration=Math.max(.001,Number(media.duration)||Number(media.durationSec)||4);if(media.kind==='audio')draft.audioItems=[...draft.audioItems,{id:'audio-'+String(revision+1)+'-'+text(media.assetId),trackId:'audio',start:0,duration,trimStart:0,volume:1,asset:media,url:media.url}];else{const start=draft.clips.reduce((end,item)=>Math.max(end,(Number(item.start)||0)+(Number(item.duration)||0)),0);draft.clips=[...draft.clips,{id:'clip-'+String(revision+1)+'-'+text(media.assetId),trackId:'video',start,duration,trimStart:0,speed:1,volume:1,asset:media,url:media.url}]};changed();draw();return}if(kind==='move-clip-up'&&index>0){const target=draft.clips.find((item,position)=>position===index-1);draft.clips=draft.clips.map((item,position)=>position===index-1?clip:position===index?target:item);changed();draw();return}if(kind==='move-clip-down'&&index<draft.clips.length-1){const target=draft.clips.find((item,position)=>position===index+1);draft.clips=draft.clips.map((item,position)=>position===index+1?clip:position===index?target:item);changed();draw();return}if(kind==='duplicate-clip'&&clip){const next={...clone(clip),id:text(clip.id)+'-copy-'+String(revision+1),start:(Number(clip.start)||0)+(Number(clip.duration)||0)};draft.clips=[...draft.clips.slice(0,index+1),next,...draft.clips.slice(index+1)];changed();draw();return}if(kind==='split-clip'&&clip){const duration=Math.max(.001,Number(clip.duration)||.001),half=duration/2,second={...clone(clip),id:text(clip.id)+'-split-'+String(revision+1),start:(Number(clip.start)||0)+half,duration:half,trimStart:(Number(clip.trimStart)||0)+half};clip.duration=half;draft.clips=[...draft.clips.slice(0,index+1),second,...draft.clips.slice(index+1)];changed();draw();return}if(kind==='remove-clip'){draft.clips=draft.clips.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-audio'){draft.audioItems=draft.audioItems.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-text'){draft.textOverlays=draft.textOverlays.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-caption'){draft.captions=draft.captions.filter((item,position)=>position!==index);changed();draw();return}if(kind==='remove-effect'){draft.effects=draft.effects.filter((item,position)=>position!==index);changed();draw();return}\\n if(clip){if(kind==='clip-track')clip.trackId=event.value;if(kind==='clip-start')clip.start=Math.max(0,Number(event.value)||0);if(kind==='clip-duration')clip.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='clip-trim')clip.trimStart=Math.max(0,Number(event.value)||0);if(kind==='clip-speed')clip.speed=Math.max(0.01,Number(event.value)||1);if(kind==='clip-volume')clip.volume=Math.max(0,Number(event.value)||0);if(kind==='clip-transition'||kind==='clip-transition-duration'){if(!clip.transition||Array.isArray(clip.transition))clip.transition={type:'Cut',duration:0};if(kind==='clip-transition')clip.transition.type=event.value;else clip.transition.duration=Math.max(0,Number(event.value)||0)}if(kind==='clip-opacity'){if(!clip.transform||Array.isArray(clip.transform))clip.transform={};clip.transform.opacity=Math.max(0,Math.min(1,Number(event.value)||0))}if(kind==='clip-reverse')clip.reverse=event.checked;if(kind==='clip-motion'||kind==='clip-easing'){if(!clip.motion||Array.isArray(clip.motion))clip.motion={type:'none',easing:'ease-in-out'};if(kind==='clip-motion')clip.motion.type=event.value;else clip.motion.easing=event.value}}\\n if(audio){if(kind==='audio-track')audio.trackId=event.value;if(kind==='audio-start')audio.start=Math.max(0,Number(event.value)||0);if(kind==='audio-duration')audio.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='audio-trim'){audio.trimStart=Math.max(0,Number(event.value)||0);audio.sourceOffset=audio.trimStart}if(kind==='audio-volume')audio.volume=Math.max(0,Number(event.value)||0);if(kind==='audio-fade-in')audio.fadeIn=Math.max(0,Number(event.value)||0);if(kind==='audio-fade-out')audio.fadeOut=Math.max(0,Number(event.value)||0);if(kind==='audio-ducking')audio.ducking=Math.max(0,Math.min(1,Number(event.value)||0));if(kind==='audio-rate')audio.rate=Math.max(.05,Math.min(4,Number(event.value)||1));if(kind==='audio-loop')audio.loop=event.checked;if(kind==='audio-muted')audio.muted=event.checked}\\n if(title){if(kind==='text-value')title.text=event.value;if(kind==='text-track')title.trackId=event.value;if(kind==='text-start')title.start=Math.max(0,Number(event.value)||0);if(kind==='text-duration')title.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='text-animation')title.animation=event.value}\\n if(caption){if(kind==='caption-value')caption.text=event.value;if(kind==='caption-track')caption.trackId=event.value;if(kind==='caption-start')caption.start=Math.max(0,Number(event.value)||0);if(kind==='caption-end')caption.end=Math.max(caption.start+0.001,Number(event.value)||caption.start+0.001);if(kind==='caption-animation')caption.animation=event.value}\\n if(effect){if(kind==='effect-type')effect.type=event.value;if(kind==='effect-track')effect.trackId=event.value;if(kind==='effect-start')effect.start=Math.max(0,Number(event.value)||0);if(kind==='effect-duration')effect.duration=Math.max(0.001,Number(event.value)||0.001);if(kind==='effect-intensity')effect.intensity=Number(event.value)}changed()});\\nbuild.addEventListener('click',()=>{if(!busy){draft=assemble();changed();draw();status.textContent='Built an editable timeline from current project media.'}});\\ndocument.getElementById('addText').addEventListener('click',()=>{if(busy)return;ensure();draft.textOverlays=[...draft.textOverlays,{id:'portable-text-'+String(draft.textOverlays.length+1)+'-'+String(revision+1),trackId:'text',text:'New text',start:0,duration:2,animation:'none'}];changed();draw()});\\ndocument.getElementById('addCaption').addEventListener('click',()=>{if(busy)return;ensure();draft.captions=[...draft.captions,{id:'portable-caption-'+String(draft.captions.length+1)+'-'+String(revision+1),trackId:'captions',text:'New caption',start:0,end:2,font:'Hanken Grotesk',animation:'none'}];changed();draw()});\\ndocument.getElementById('addEffect').addEventListener('click',()=>{if(busy)return;ensure();draft.effects=[...draft.effects,{id:'portable-effect-'+String(draft.effects.length+1)+'-'+String(revision+1),trackId:'video',type:'fade',start:0,duration:1,intensity:1}];changed();draw()});\\nplayhead.addEventListener('input',composition);\\ndocument.getElementById('undo').addEventListener('click',()=>{if(busy||editStackIndex<=0)return;editStackIndex-=1;draft=clone(editStack.find((item,index)=>index===editStackIndex));revision+=1;draw();status.textContent='Undid the latest local change.'});\\ndocument.getElementById('redo').addEventListener('click',()=>{if(busy||editStackIndex>=editStack.length-1)return;editStackIndex+=1;draft=clone(editStack.find((item,index)=>index===editStackIndex));revision+=1;draw();status.textContent='Restored the next local change.'});\\ndocument.getElementById('snapshot').addEventListener('click',()=>{if(busy)return;ensure();const id='timeline-version-'+String(Date.now())+'-'+String(revision+1),version={id,label:'Timeline version '+String((draft.timelineVersions||[]).length+1),createdAt:new Date().toISOString(),clips:clone(draft.clips),audioItems:clone(draft.audioItems),captions:clone(draft.captions),textOverlays:clone(draft.textOverlays),effects:clone(draft.effects)};draft.timelineVersions=[...(draft.timelineVersions||[]),version].slice(-50);changed();draw();status.textContent='Saved a restorable timeline version in this Editor document.'});\\ndocument.getElementById('applyRaw').addEventListener('click',()=>{if(busy)return;try{const value=JSON.parse(raw.value);if(!value||Array.isArray(value)||value.schemaVersion!==1||typeof value.activeTimelineId!=='string')throw Error('Editor JSON must be a version 1 document with an active timeline ID.');draft=clone(value);changed();draw();status.textContent='Complete Editor data applied locally.'}catch(error){status.textContent=error.message}});\\ndocument.getElementById('reset').addEventListener('click',()=>{if(!busy){draft=clone(source);revision+=1;draw();status.textContent='Changes reset to the current Editor timeline.'}});\\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Editor timeline…';try{const result=await StillMade.run({editor:source,updatedEditor:draft,commit:true});draft=clone(result.editor);if(started===revision)status.textContent='Editor changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'Editor changes could not be prepared.'}finally{busy=false;draw()}});\\nStillMade.onInput(values=>{source=clone(values.editor||source);assets=clone(values.assets||[]);draft=values.updatedEditor&&(values.updatedEditor.clips?.length||values.updatedEditor.audioItems?.length||values.updatedEditor.timelineVersions?.length)?clone(values.updatedEditor):source.clips?.length||source.audioItems?.length?clone(source):assemble();revision+=1;editStack=[clone(draft)];editStackIndex=0;playhead.value='0';draw();status.textContent='Edit the complete timeline. Motion graphics and every other Editor tool stay inside this Block.'});\\n\\ndocument.getElementById('renderFinal').addEventListener('click',async()=>{if(busy)return;busy=true;draw();status.textContent='Preparing the exact final render…';try{await StillMade.run({editor:source,updatedEditor:draft,commit:false,action:'render',renderFps:Number(document.getElementById('renderFps').value),renderResolution:Number(document.getElementById('renderResolution').value),renderQuality:document.getElementById('renderQuality').value});status.textContent='Final render is ready below. Choose Render final file to start the device renderer.'}catch(error){status.textContent=error.message||'The render plan could not be prepared.'}finally{busy=false;draw()}});\\n\"\n}"},"lineage":{"parents":[],"changes":"Packaged original Block source for StillMade."}}}],"license":"MIT"}