{"_id":"@ariefsn/svelte-video-editor","_rev":"3-d8ec6ab339e7d9737e3b509e31262426","name":"@ariefsn/svelte-video-editor","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@ariefsn/svelte-video-editor","version":"1.0.0","keywords":["svelte","svelte5","sveltekit","video-editor","video-timeline","timeline","timeline-editor","nle","video","editor","clips","tracks","trimming","ripple-edit","roll-edit","slip-edit","transitions","markers","undo-redo","tailwindcss","typescript","component-library"],"author":{"name":"Arief Setiyo Nugroho","email":"me@ariefsn.dev"},"license":"MIT","_id":"@ariefsn/svelte-video-editor@1.0.0","maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"homepage":"https://github.com/ariefsn/svelte-video-editor#readme","bugs":{"url":"https://github.com/ariefsn/svelte-video-editor/issues"},"dist":{"shasum":"3346df447cce607d294daccf896f598b86d717de","tarball":"https://registry.npmjs.org/@ariefsn/svelte-video-editor/-/svelte-video-editor-1.0.0.tgz","fileCount":110,"integrity":"sha512-6WUidM9IqtbPI6OW0TKqj6ghC9xeBe5R4uj8Et1OyGsyMkkHY+TvngWtPa0d/0xN7y1k0nwv8M3AkBDtdKpCIw==","signatures":[{"sig":"MEQCIHGG+usviYqnq9mtTu2dltnQd/elA6cajxe+EhdRf1smAiAUM1C/A4gxOoL/kKc3vfYkgyEs3ERrGiM87G+7J6xd9w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":321130},"type":"module","types":"./dist/index.d.ts","svelte":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"},"./app.css":"./dist/app.css"},"gitHead":"f9d819ff34f766dfc8acc2c5ee138d4f7c24b70f","scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","test:unit":"vitest","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"},"_npmUser":{"name":"ariefsn","email":"ayiexz22@gmail.com"},"repository":{"url":"git+https://github.com/ariefsn/svelte-video-editor.git","type":"git"},"_npmVersion":"11.13.0","description":"A host-agnostic Svelte 5 video timeline editor — clips, tracks, trimming, ripple/roll/slip edits, grouping, linked audio, markers, undo/redo and a real-time preview. Zero design-system dependency.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.16.0","dependencies":{"clsx":"^2.1.1","@lucide/svelte":"^1.18.0","tailwind-merge":"^3.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","eslint":"^10.4.1","svelte":"^5.56.1","vitest":"^4.1.8","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^22","tailwindcss":"^4.3.0","svelte-check":"^4.6.0","@sveltejs/kit":"^2.63.0","@sveltejs/package":"^2.5.8","@tailwindcss/vite":"^4.3.0","typescript-eslint":"^8.60.1","eslint-plugin-svelte":"^3.19.0","vitest-browser-svelte":"^2.1.1","@sveltejs/adapter-auto":"^7.0.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^4.1.0","@tailwindcss/typography":"^0.5.19","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.8","prettier-plugin-tailwindcss":"^0.8.0","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/svelte-video-editor_1.0.0_1781444756515_0.7886294394441855","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@ariefsn/svelte-video-editor","version":"1.1.0","keywords":["svelte","svelte5","sveltekit","video-editor","video-timeline","timeline","timeline-editor","nle","video","editor","clips","tracks","trimming","ripple-edit","roll-edit","slip-edit","transitions","markers","undo-redo","tailwindcss","typescript","component-library"],"author":{"name":"Arief Setiyo Nugroho","email":"me@ariefsn.dev"},"license":"MIT","_id":"@ariefsn/svelte-video-editor@1.1.0","maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"homepage":"https://github.com/ariefsn/svelte-video-editor#readme","bugs":{"url":"https://github.com/ariefsn/svelte-video-editor/issues"},"dist":{"shasum":"ff8cd449a75e761909fa68d72d5403e87a916615","tarball":"https://registry.npmjs.org/@ariefsn/svelte-video-editor/-/svelte-video-editor-1.1.0.tgz","fileCount":114,"integrity":"sha512-6x4ZU4HmUtrwPdy4tQ9nvq+eRrCPtYUZL9zRk3n7yGiRBjiV7UvtQQnTxihPQJ3e9w/8T497IqRAD6S3iU8Tkw==","signatures":[{"sig":"MEYCIQCTGUU+iCJo6QamMPNrJMGuHCDnVWU0oNFds4Imyo6CtQIhAOwZ3GkTF2su39wMmyoKf/Y24uc/teCS54tYRShH97NA","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":341451},"type":"module","types":"./dist/index.d.ts","svelte":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"},"./app.css":"./dist/app.css"},"gitHead":"36bd2d733fcdf71d29585b8d56f9bdbe9b9888f0","scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","test:unit":"vitest","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"},"_npmUser":{"name":"ariefsn","email":"ayiexz22@gmail.com"},"repository":{"url":"git+https://github.com/ariefsn/svelte-video-editor.git","type":"git"},"_npmVersion":"11.13.0","description":"A host-agnostic Svelte 5 video timeline editor — clips, tracks, trimming, ripple/roll/slip edits, grouping, linked audio, markers, undo/redo and a real-time preview. Zero design-system dependency.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.16.0","dependencies":{"clsx":"^2.1.1","@lucide/svelte":"^1.18.0","tailwind-merge":"^3.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","eslint":"^10.4.1","svelte":"^5.56.1","vitest":"^4.1.8","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^22","tailwindcss":"^4.3.0","svelte-check":"^4.6.0","@sveltejs/kit":"^2.63.0","@sveltejs/package":"^2.5.8","@tailwindcss/vite":"^4.3.0","typescript-eslint":"^8.60.1","eslint-plugin-svelte":"^3.19.0","vitest-browser-svelte":"^2.1.1","@sveltejs/adapter-auto":"^7.0.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^4.1.0","@tailwindcss/typography":"^0.5.19","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.8","prettier-plugin-tailwindcss":"^0.8.0","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/svelte-video-editor_1.1.0_1781489036177_0.0972352191110426","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"_id":"@ariefsn/svelte-video-editor@1.2.0","bugs":{"url":"https://github.com/ariefsn/svelte-video-editor/issues"},"dist":{"shasum":"a44e3d4de451464c6691ed4cd247e8993397f253","tarball":"https://registry.npmjs.org/@ariefsn/svelte-video-editor/-/svelte-video-editor-1.2.0.tgz","fileCount":118,"integrity":"sha512-xwBHSAaahhwJmXkBJmfLQGiyaazY6Nj1wyeBzro5Bk1cgeQkZI+mw2PUhld5q3qZXTZ1PPRYYk2tZttSYCZ33Q==","signatures":[{"sig":"MEUCIQDrpwEX4bTywwFF3+/MudONsOocQZjW2Z0ZaF0nL2TdCQIgbMb9U9n90rKf3lkpu2FmP4Mj/bmCnmtyGF0PxrlGbkc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCEMRmVBoZlTSxNXSFTCf41QH6sziKfj/GV85n0VM2jugIhAMXGbqLKsA2cTbokHJ3wklHOKh4krwFtfNls1kCEu+BR"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ariefsn%2fsvelte-video-editor@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":361658},"name":"@ariefsn/svelte-video-editor","type":"module","types":"./dist/index.d.ts","author":{"name":"Arief Setiyo Nugroho","email":"me@ariefsn.dev"},"svelte":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"},"./pure":{"types":"./dist/pure.d.ts","default":"./dist/pure.js"},"./app.css":"./dist/app.css"},"gitHead":"4b29e429d97b8014f3da8812a9b8a7ab8a9947b2","license":"MIT","scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","test:unit":"vitest","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"},"version":"1.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e5162720-87cc-42b2-877b-cd632ba7c059"}},"homepage":"https://github.com/ariefsn/svelte-video-editor#readme","keywords":["svelte","svelte5","sveltekit","video-editor","video-timeline","timeline","timeline-editor","nle","video","editor","clips","tracks","trimming","ripple-edit","roll-edit","slip-edit","transitions","markers","undo-redo","tailwindcss","typescript","component-library"],"repository":{"url":"git+https://github.com/ariefsn/svelte-video-editor.git","type":"git"},"_npmVersion":"11.19.0","description":"A host-agnostic Svelte 5 video timeline editor — clips, tracks, trimming, ripple/roll/slip edits, grouping, linked audio, markers, undo/redo and a real-time preview. Zero design-system dependency.","directories":{},"maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"sideEffects":["**/*.css"],"_nodeVersion":"24.21.0","dependencies":{"clsx":"^2.1.1","@lucide/svelte":"^1.18.0","tailwind-merge":"^3.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","eslint":"^10.4.1","svelte":"^5.56.1","vitest":"^4.1.8","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^22","tailwindcss":"^4.3.0","svelte-check":"^4.6.0","@sveltejs/kit":"^2.63.0","@sveltejs/package":"^2.5.8","@tailwindcss/vite":"^4.3.0","typescript-eslint":"^8.60.1","eslint-plugin-svelte":"^3.19.0","vitest-browser-svelte":"^2.1.1","@sveltejs/adapter-auto":"^7.0.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^4.1.0","@tailwindcss/typography":"^0.5.19","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.8","prettier-plugin-tailwindcss":"^0.8.0","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/svelte-video-editor_1.2.0_1790615446709_0.3504536031584786"}}},"time":{"created":"2026-06-14T13:45:56.395Z","modified":"2026-09-28T17:10:47.284Z","1.0.0":"2026-06-14T13:45:56.687Z","1.1.0":"2026-06-15T02:03:56.360Z","1.2.0":"2026-09-28T17:10:46.867Z"},"bugs":{"url":"https://github.com/ariefsn/svelte-video-editor/issues"},"author":{"name":"Arief Setiyo Nugroho","email":"me@ariefsn.dev"},"license":"MIT","homepage":"https://github.com/ariefsn/svelte-video-editor#readme","keywords":["svelte","svelte5","sveltekit","video-editor","video-timeline","timeline","timeline-editor","nle","video","editor","clips","tracks","trimming","ripple-edit","roll-edit","slip-edit","transitions","markers","undo-redo","tailwindcss","typescript","component-library"],"repository":{"url":"git+https://github.com/ariefsn/svelte-video-editor.git","type":"git"},"description":"A host-agnostic Svelte 5 video timeline editor — clips, tracks, trimming, ripple/roll/slip edits, grouping, linked audio, markers, undo/redo and a real-time preview. Zero design-system dependency.","maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"readme":"<p align=\"center\">\n  <img src=\"./static/logo.svg\" alt=\"svelte-video-editor\" width=\"128\" height=\"128\" />\n</p>\n\n<h1 align=\"center\">@ariefsn/svelte-video-editor</h1>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@ariefsn/svelte-video-editor\"><img alt=\"npm version\" src=\"https://img.shields.io/npm/v/@ariefsn/svelte-video-editor?color=cb3837&logo=npm\"></a>\n  <a href=\"https://www.npmjs.com/package/@ariefsn/svelte-video-editor\"><img alt=\"npm downloads\" src=\"https://img.shields.io/npm/dm/@ariefsn/svelte-video-editor?color=cb3837&logo=npm\"></a>\n  <a href=\"https://bundlephobia.com/package/@ariefsn/svelte-video-editor\"><img alt=\"minzipped size\" src=\"https://img.shields.io/bundlephobia/minzip/@ariefsn/svelte-video-editor\"></a>\n  <img alt=\"svelte 5\" src=\"https://img.shields.io/badge/svelte-5-ff3e00?logo=svelte&logoColor=white\">\n  <img alt=\"types included\" src=\"https://img.shields.io/npm/types/@ariefsn/svelte-video-editor\">\n  <a href=\"./LICENSE\"><img alt=\"license MIT\" src=\"https://img.shields.io/npm/l/@ariefsn/svelte-video-editor?color=blue\"></a>\n</p>\n\nA host-agnostic, **Svelte 5** video timeline editor — a CapCut/Premiere-style timeline with\nclips, tracks, trimming, ripple/roll/slip edits, grouping, linked audio, markers, in/out range,\nundo/redo, and a real-time DOM preview. No framework lock-in: all external concerns (asset URLs,\nthumbnails, export, permissions, persistence, notifications, confirmation, i18n) are injected by\nthe host.\n\n- **Built-in clip transitions** — per-clip enter/exit animations (fade, slide, scale, zoom,\n  bounce, pop, spin, blur, wipe, flip) with duration + easing, scrubbed live by the playhead.\n- **Responsive & mobile-ready** — the bin and clip inspector collapse into bottom sheets, the\n  toolbar folds into an overflow menu, and clip options are reachable by touch.\n- **The library owns no storage** — you persist the project (and pane height) however you like.\n  Every clip's `animation` rides along in the project JSON, so the host can render it any way it\n  likes on export.\n- **Tailwind v4 theming** — ships a simple light/dark theme you can override via CSS variables.\n\n> ⚠️ **Browser-only.** This component renders in the browser (DOM, `<video>`/`<audio>`, canvas,\n> `requestAnimationFrame`). It is safe to _import_ on the server, but render it client-side only\n> (see [SvelteKit (SSR)](#sveltekit-ssr)).\n\n- **Demo:** [https://svelte-video-editor.ariefsn.dev](https://svelte-video-editor.ariefsn.dev)\n- **Repo:** [https://github.com/ariefsn/svelte-video-editor](https://github.com/ariefsn/svelte-video-editor)\n\n## Contents\n\n- [Install](#install)\n- [Theme](#theme)\n- [Simple mode](#simple-mode) — props & callbacks only\n- [Advanced mode](#advanced-mode) — snippets, i18n, host-owned state\n- [Opening media](#opening-media) — file picker, drag-and-drop, `binImport`\n- [SvelteKit (SSR)](#sveltekit-ssr)\n- [Host contract](#host-contract)\n- [Clip transitions](#clip-transitions) — built-in enter/exit animations\n- [Section overrides](#section-overrides-snippets)\n- [Custom preview renderer](#custom-preview-renderer) — Remotion, canvas, anything\n- [Localization](#localization)\n- [License](#license)\n\n## Install\n\n```bash\nbun add @ariefsn/svelte-video-editor\n# or: npm i @ariefsn/svelte-video-editor\n# or: pnpm add @ariefsn/svelte-video-editor\n```\n\nRequires `svelte@^5` and Tailwind v4 in your app.\n\n## Theme\n\nImport the shipped theme once (it defines the `--ts-*` CSS variables + light/dark palette):\n\n```ts\n// app.css or your root layout\nimport '@ariefsn/svelte-video-editor/app.css';\n```\n\nDark mode follows a `.dark` class on an ancestor (e.g. `<html class=\"dark\">`). Override any token\nto restyle:\n\n```css\n:root {\n\t--ts-primary: hsl(220 90% 56%); /* your brand */\n}\n```\n\n---\n\n## Simple mode\n\nThe minimal \"drop it in and go\" setup — **props and callbacks only, no snippets**. You supply a\nproject, persist it in `onChange`, and resolve media. That's it.\n\n```svelte\n<script lang=\"ts\">\n\timport { browser } from '$app/environment';\n\timport {\n\t\tTimelineEditor,\n\t\tcreateEmptyProject,\n\t\tmigrateProject,\n\t\ttype NotifyKind,\n\t\ttype ResolvedAsset,\n\t\ttype TimelineProject\n\t} from '@ariefsn/svelte-video-editor';\n\timport '@ariefsn/svelte-video-editor/app.css';\n\n\t// The host owns the project and its persistence. The library stores nothing.\n\tlet project = $state<TimelineProject | null>(null);\n\n\t$effect(() => {\n\t\tif (!browser || project) return;\n\t\tconst saved = localStorage.getItem('my-project');\n\t\tproject = saved ? migrateProject(JSON.parse(saved)) : createEmptyProject('My first video');\n\t});\n\n\tfunction handleChange(p: TimelineProject) {\n\t\tproject = p;\n\t\tlocalStorage.setItem('my-project', JSON.stringify(p)); // host-owned persistence\n\t}\n\n\t// Resolve a clip's media URL (+ metadata). Here URLs resolve to themselves.\n\tasync function resolveAsset(assetId: string): Promise<ResolvedAsset> {\n\t\treturn { url: assetId, hasAudio: true };\n\t}\n\n\t// Return a thumbnail image URL for `frame` (at a fixed 30fps reference).\n\tasync function generateThumbnail(assetId: string, frame: number): Promise<string> {\n\t\treturn assetId; // e.g. an <img> poster; real apps seek a <video> + canvas\n\t}\n\n\tfunction handleExport(p: TimelineProject) {\n\t\tconsole.log('export', p); // you own rendering\n\t}\n\n\tfunction handleNotify(message: string, kind: NotifyKind) {\n\t\tconsole.log(kind, message); // wire to your toast system if you like\n\t}\n</script>\n\n{#if browser && project}\n\t{#key project.id}\n\t\t<TimelineEditor\n\t\t\t{project}\n\t\t\tonChange={handleChange}\n\t\t\t{resolveAsset}\n\t\t\t{generateThumbnail}\n\t\t\tonExport={handleExport}\n\t\t\tcan={() => true}\n\t\t\tonNotify={handleNotify}\n\t\t/>\n\t{/key}\n{/if}\n```\n\n`onBack` is omitted here, so no back button renders.\n\n---\n\n## Advanced mode\n\nUse snippets to customize sections, supply your own confirm dialog, switch languages at runtime,\ngate features, and own the pane height — everything injected by the host.\n\n```svelte\n<script lang=\"ts\">\n\timport { browser } from '$app/environment';\n\timport {\n\t\tTimelineEditor,\n\t\tInspectorPanel,\n\t\tcreateEmptyProject,\n\t\tuid,\n\t\ttype BinItem,\n\t\ttype ConfirmOptions,\n\t\ttype EditorAction,\n\t\ttype MessagesOverride,\n\t\ttype ResolvedAsset,\n\t\ttype SectionCtx,\n\t\ttype TimelineProject\n\t} from '@ariefsn/svelte-video-editor';\n\timport '@ariefsn/svelte-video-editor/app.css';\n\n\tlet project = $state<TimelineProject | null>(null);\n\tlet timelineHeight = $state<number | undefined>(undefined);\n\tlet lang = $state<'en' | 'id'>('en');\n\tlet proTier = $state(true);\n\n\t$effect(() => {\n\t\tif (!browser || project) return;\n\t\tproject = createEmptyProject('Advanced demo');\n\t\ttimelineHeight = Number(localStorage.getItem('paneH')) || undefined;\n\t});\n\n\tasync function resolveAsset(assetId: string): Promise<ResolvedAsset> {\n\t\treturn { url: assetId, hasAudio: true };\n\t}\n\tasync function generateThumbnail(assetId: string): Promise<string> {\n\t\treturn assetId;\n\t}\n\n\tfunction handleChange(p: TimelineProject) {\n\t\tproject = p;\n\t}\n\n\tfunction handleExport(p: TimelineProject) {\n\t\tconsole.log('export', p);\n\t}\n\n\tfunction handleHeight(h: number) {\n\t\ttimelineHeight = h;\n\t\tlocalStorage.setItem('paneH', String(h)); // host-owned persistence\n\t}\n\n\tfunction canDo(action: EditorAction): boolean {\n\t\treturn action === 'export' ? proTier : true;\n\t}\n\n\t// Localization is host-owned: swap the whole messages map to switch language.\n\tconst messagesId: MessagesOverride = { export: 'Ekspor', split: 'Pisah', undo: 'Urungkan' };\n\tconst messages = $derived<MessagesOverride | undefined>(lang === 'id' ? messagesId : undefined);\n\n\t// Use your OWN confirm dialog instead of the built-in one (optional).\n\tasync function confirm(opts: ConfirmOptions): Promise<boolean> {\n\t\treturn window.confirm(`${opts.title}\\n\\n${opts.message ?? ''}`);\n\t}\n\n\tlet pasteUrl = $state('');\n\tfunction addByUrl(addItems: (items: BinItem[]) => void) {\n\t\tif (!pasteUrl.trim()) return;\n\t\taddItems([{ id: uid(), url: pasteUrl, name: 'media', mediaType: 'video', duration: null }]);\n\t\tpasteUrl = '';\n\t}\n</script>\n\n{#if browser && project}\n\t{#key project.id}\n\t\t<TimelineEditor\n\t\t\t{project}\n\t\t\tonChange={handleChange}\n\t\t\t{resolveAsset}\n\t\t\t{generateThumbnail}\n\t\t\tonExport={handleExport}\n\t\t\tcan={canDo}\n\t\t\t{messages}\n\t\t\t{confirm}\n\t\t\t{timelineHeight}\n\t\t\tonTimelineHeightChange={handleHeight}\n\t\t\tonBack={() => history.back()}\n\t\t>\n\t\t\t<!-- Host-owned import UI inside the asset bin -->\n\t\t\t{#snippet binImport({ addItems })}\n\t\t\t\t<form\n\t\t\t\t\tonsubmit={(e) => {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\taddByUrl(addItems);\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<input bind:value={pasteUrl} placeholder=\"Paste media URL…\" />\n\t\t\t\t\t<button>Add</button>\n\t\t\t\t</form>\n\t\t\t{/snippet}\n\n\t\t\t<!-- WRAP a default section: render the built-in + add your own UI -->\n\t\t\t{#snippet inspector(ctx: SectionCtx)}\n\t\t\t\t{#if ctx.editor.activeClip}\n\t\t\t\t\t<InspectorPanel onRequestDelete={ctx.onRequestDelete} />\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"p-3 text-xs\">Active clip: {ctx.editor.activeClip?.name ?? '—'}</div>\n\t\t\t{/snippet}\n\n\t\t\t<!-- FULLY REPLACE a section -->\n\t\t\t{#snippet shortcutsFooter()}\n\t\t\t\t<div class=\"border-t px-3 py-1 text-xs\">My custom footer</div>\n\t\t\t{/snippet}\n\t\t</TimelineEditor>\n\t{/key}\n{/if}\n```\n\nSwitch `lang` between `'en'` and `'id'` at runtime and the whole UI re-labels reactively — no\n`locale` prop, the `messages` map is the single source of translation.\n\n> A runnable version of both modes lives in this repo under `src/routes/` (`/` = simple,\n> `/advanced` = advanced). Run `bun run dev`.\n\n---\n\n## Opening media\n\n**The editor ships no import UI on purpose.** It never touches files, never fetches and never\nbuilds storage URLs, so _you_ decide where media comes from — an upload endpoint, an existing\nasset library, a stock picker, or the local disk. That UI goes in the `binImport` snippet, which\nreceives `addItems(items: BinItem[])` and renders inside the asset bin.\n\nUntil you supply it, **the bin stays empty and there is no way for a user to add media** — that is\nthe contract, not a bug.\n\n### A local-file picker (click to browse + drop files)\n\n```svelte\n<script lang=\"ts\">\n\timport { uid, type BinItem } from '@ariefsn/svelte-video-editor';\n\n\tlet fileInput: HTMLInputElement | null = $state(null);\n\tlet dragging = $state(false);\n\n\t// Keep blob URLs OUT of the saved project: register each file under a\n\t// stable assetId, and let resolveAsset hand back the live URL.\n\tconst localAssets = new Map<string, string>();\n\n\tasync function handleFiles(files: FileList | null, addItems: (i: BinItem[]) => void) {\n\t\tif (!files?.length) return;\n\t\tconst items: BinItem[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst mediaType = file.type.startsWith('video/')\n\t\t\t\t? 'video'\n\t\t\t\t: file.type.startsWith('audio/')\n\t\t\t\t\t? 'audio'\n\t\t\t\t\t: 'image';\n\t\t\tconst url = URL.createObjectURL(file);\n\t\t\tconst assetId = `local:${uid()}`;\n\t\t\tlocalAssets.set(assetId, url);\n\t\t\titems.push({ id: uid(), assetId, url, name: file.name, mediaType, duration: null });\n\t\t}\n\t\taddItems(items);\n\t}\n</script>\n\n{#snippet binImport({ addItems })}\n\t<!-- preventDefault on dragover, or the browser navigates to the dropped file -->\n\t<div\n\t\tondragover={(e) => {\n\t\t\te.preventDefault();\n\t\t\tdragging = true;\n\t\t}}\n\t\tondragleave={() => (dragging = false)}\n\t\tondrop={(e) => {\n\t\t\te.preventDefault();\n\t\t\tdragging = false;\n\t\t\thandleFiles(e.dataTransfer?.files ?? null, addItems);\n\t\t}}\n\t\tclass={dragging ? 'ring-2' : ''}\n\t>\n\t\t<input\n\t\t\tbind:this={fileInput}\n\t\t\ttype=\"file\"\n\t\t\tmultiple\n\t\t\taccept=\"video/*,audio/*,image/*\"\n\t\t\thidden\n\t\t\tonchange={(e) => {\n\t\t\t\thandleFiles(e.currentTarget.files, addItems);\n\t\t\t\te.currentTarget.value = ''; // so re-picking the same file re-fires\n\t\t\t}}\n\t\t/>\n\t\t<button onclick={() => fileInput?.click()}>Open media</button>\n\t\t<p>or drop files here</p>\n\t</div>\n{/snippet}\n```\n\nPair it with a `resolveAsset` that knows about those ids:\n\n```ts\nasync function resolveAsset(assetId: string) {\n\treturn { url: localAssets.get(assetId) ?? assetId, hasAudio: true };\n}\n```\n\n> ⚠️ **`blob:` URLs must never be persisted.** They die when the tab closes, so a project saved\n> with one comes back pointing at media that can never load. Store the `assetId` and resolve a\n> fresh URL each session — strip `blob:` values in your `onChange` handler before writing them\n> anywhere. For media that must survive a reload, upload the file and use the uploaded URL.\n\nSet `duration` (source length in **seconds**) when you know it, and clips get the right length on\nthe timeline; leave it `null` and the editor falls back to a default. You can probe it from a\ndetached `<video>`/`<audio>` on `loadedmetadata`.\n\nSee [`src/routes/advanced/OpenMediaControl.svelte`](./src/routes/advanced/OpenMediaControl.svelte)\nfor the complete version used by the `/advanced` demo, including duration probing and\nsession-expiry handling.\n\n---\n\n## SvelteKit (SSR)\n\nSvelteKit is SSR by default and the editor needs the DOM — render it under a browser guard:\n\n```svelte\n<script lang=\"ts\">\n\timport { browser } from '$app/environment';\n\timport { TimelineEditor } from '@ariefsn/svelte-video-editor';\n</script>\n\n{#if browser}\n\t<TimelineEditor … />\n{/if}\n```\n\n(Or set `export const ssr = false` on the route.)\n\n---\n\n## Host contract\n\n| Prop                                        | Type                                  | Required | Notes                                                                               |\n| ------------------------------------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------- |\n| `project`                                   | `TimelineProject`                     | ✅       | Auto-migrated from older shapes. Remount via `{#key project.id}` to switch.         |\n| `onChange`                                  | `(project) => void`                   | ✅       | Debounced (`changeDebounceMs`, default 800ms; `0` = immediate). **You persist it.** |\n| `resolveAsset`                              | `(assetId) => Promise<ResolvedAsset>` | ✅       | Resolve media URL + metadata. See [Opening media](#opening-media).                  |\n| `generateThumbnail`                         | `(assetId, frame) => Promise<string>` | ✅       | `frame` at a fixed 30fps reference.                                                 |\n| `onExport`                                  | `(project, range?) => void`           | ✅       | You own rendering/export.                                                           |\n| `can`                                       | `(action) => boolean`                 | ✅       | Permission gate (`'export'`, `'magnetic-main-track'`).                              |\n| `onBack`                                    | `() => void`                          | ❌       | Back button renders only when provided.                                             |\n| `onNotify`                                  | `(message, kind) => void`             | ❌       | Transient feedback sink; no-ops if absent.                                          |\n| `messages`                                  | `Partial<Messages>`                   | ❌       | Override/translate any label (the single i18n source).                              |\n| `confirm`                                   | `(opts) => Promise<boolean>`          | ❌       | Use your own confirm dialog instead of the built-in.                                |\n| `timelineHeight` / `onTimelineHeightChange` | `number` / `(h) => void`              | ❌       | Host-owned timeline pane height.                                                    |\n| `binWidth` / `onBinWidthChange`             | `number` / `(w) => void`              | ❌       | Host-owned bin column width (desktop; draggable separator).                         |\n| `inspectorWidth` / `onInspectorWidthChange` | `number` / `(w) => void`              | ❌       | Host-owned inspector column width (desktop; draggable separator).                   |\n| `magneticMainTrack`                         | `boolean`                             | ❌       | CapCut-style packing on the first track.                                            |\n| `changeDebounceMs`                          | `number`                              | ❌       | Debounce for `onChange`.                                                            |\n\n`ProjectListView` is also exported for a \"pick/create a project\" screen (`projects`, `onOpen`,\n`onCreate`, `onRename`, `onDelete`, plus optional `messages` / `confirm`).\n\n## Clip transitions\n\nEvery clip (video, image, text) can carry an optional **enter** and **exit** transition, edited from\nthe inspector's **Animation** section and previewed live (the playhead scrubs them). The data is a\nplain field on the clip, so it round-trips through `onChange` / `onExport` for the host to render.\n\n```ts\nimport {\n\ttype ClipAnimation,\n\ttype ClipTransition,\n\ttype AnimPreset, // 'fade' | 'slide-left'|'slide-right'|'slide-up'|'slide-down'\n\t// | 'scale' | 'zoom' | 'bounce' | 'pop' | 'spin' | 'blur' | 'wipe' | 'flip'\n\ttype Easing, // 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'\n\tANIM_PRESETS,\n\tEASINGS,\n\tdefaultTransition,\n\tclipAnimStyle // pure: (clip, playheadSec, fps) => { opacity, transform, filter, clipPath }\n} from '@ariefsn/svelte-video-editor';\n\n// On each clip:  clip.animation?: { in?: ClipTransition; out?: ClipTransition }\n// ClipTransition = { preset: AnimPreset; durationF: number; easing: Easing }\n```\n\nTo render transitions yourself (e.g. a custom/Remotion compositor), call `clipAnimStyle(clip,\nplayheadSec, fps)` for the exact per-frame visual the editor preview uses — preview and export then\nmatch frame-for-frame. See [docs/clip-animations-research.md](./docs/clip-animations-research.md).\n\n## Section overrides (snippets)\n\nReplace any section via a snippet receiving `SectionCtx` (`{ editor, host, onBack?, onRequestDelete }`):\n`toolbar`, `assetBin`, `preview`, `transport`, `inspector`, `tracks`, `shortcutsFooter`, plus\n`binImport` (host import UI, gets `{ addItems }`). You can **wrap** a default by rendering the\nexported section component (e.g. `<TimelineToolbar {...ctx} />`) and adding your own UI around it.\n\n`binImport` is the one you almost certainly need: the library ships **no media-import UI at all**,\nso without it the asset bin has no way to fill. See [Opening media](#opening-media) for a\nready-made file picker and drop zone.\n\n## Custom preview renderer\n\n**Yes — you can replace the preview entirely** (Remotion, a canvas player, a WebGL compositor,\nanything). The built-in `PreviewStage` is just the default; pass a `preview` snippet and the\neditor renders yours instead. There are **two separate surfaces** to consider:\n\n| Surface                                | Override                    | What it is                                      |\n| -------------------------------------- | --------------------------- | ----------------------------------------------- |\n| **Live preview** (in-editor scrubbing) | `preview` snippet           | Real-time playback while editing.               |\n| **Final render** (export)              | `onExport(project, range?)` | You already own this — render however you like. |\n\n### What the `preview` snippet gives you\n\nThe snippet receives the same `SectionCtx` as every section override:\n\n```svelte\n<script lang=\"ts\">\n\timport { TimelineEditor, type SectionCtx } from '@ariefsn/svelte-video-editor';\n</script>\n\n<TimelineEditor {project} {onChange} {resolveAsset} {generateThumbnail} {onExport} {can}>\n\t{#snippet preview(ctx)}\n\t\t<!-- ctx.editor.project   — full reactive timeline (tracks, clips, fps, aspectRatio) -->\n\t\t<!-- ctx.editor.playhead  — current position, in seconds -->\n\t\t<!-- ctx.editor.playing   — boolean play/pause state -->\n\t\t<!-- ctx.host.resolveAsset — (assetId) => Promise<ResolvedAsset> for media URLs -->\n\t\t<MyRenderer\n\t\t\tproject={ctx.editor.project}\n\t\t\tplayhead={ctx.editor.playhead}\n\t\t\tplaying={ctx.editor.playing}\n\t\t\tresolveAsset={ctx.host.resolveAsset}\n\t\t/>\n\t{/snippet}\n</TimelineEditor>\n```\n\nAll the data types your renderer needs (`TimelineProject`, `MediaClip`, `TextClip`,\n`TimelineTrack`, `frameToSec`, `secToFrame`, `isMediaClip`, `isTextClip`, …) are exported from\nthe package.\n\n**Non-Svelte renderers** (a Remotion/React composition, a canvas/WebGL player, a server-side\nrenderer, plain Node) must import from the **`/pure`** subpath instead. The main entry only\nresolves under the `svelte` condition and pulls in `.svelte` components; `/pure` is\nframework-free and resolves in any bundler.\n\n`/pure` exposes the **same composition rules the built-in preview uses**, so any player can\nreproduce it exactly instead of re-implementing them. Times are float seconds on the project\ntimeline (`t = frame / fps`):\n\n| Question                       | Helper                                                                    |\n| ------------------------------ | ------------------------------------------------------------------------- |\n| How long is the project?       | `projectDurationF(project)`                                               |\n| What is on screen at `t`?      | `visibleClipsAt(project, t)` → `{ clip, zIndex }[]`, bottom → top         |\n| All layers (for `<Sequence>`s) | `layeredClips(project)` — z-ordered, hidden tracks skipped                |\n| Is this clip active at `t`?    | `isClipActive(clip, t, fps)` — `[start, end)`                             |\n| Where in the source media?     | `clipSourceSec(clip, t, fps)` — honours `trimInF`                         |\n| Output / stage pixel size      | `compositionSize(aspectRatio, shortSide?)`, `fitStage(aspectRatio, w, h)` |\n| Background                     | `backgroundCss(project.background)`                                       |\n| Enter/exit animation           | `clipAnimStyle(clip, t, fps)`                                             |\n| Media layer style              | `mediaClipCss(anim)` — full-bleed `object-fit: cover` + animation         |\n| Text layer style               | `textClipCss(clip.style, stageHeight, anim)` — % sizes → px               |\n| Heard at all? (mute/solo/…)    | `clipAudible(project, clip)`, `trackAudible(project, track)`              |\n| Volume at `t` (with fades)     | `clipGain(clip, t, fps)`                                                  |\n| Stored/older project           | `migrateProject(input)`                                                   |\n\nThe style helpers return camelCase CSS objects — pass them straight to a React `style` prop,\n`Object.assign(el.style, css)`, or serialize with `toCssText(css)`. A minimal DOM player:\n\n```ts\nimport {\n\tbackgroundCss,\n\tclipAnimStyle,\n\tisMediaClip,\n\tmediaClipCss,\n\ttextClipCss,\n\ttoCssText,\n\tvisibleClipsAt,\n\ttype TimelineProject\n} from '@ariefsn/svelte-video-editor/pure';\n\nfunction renderFrame(stage: HTMLElement, project: TimelineProject, t: number) {\n\tstage.style.background = backgroundCss(project.background);\n\tconst html = visibleClipsAt(project, t).map(({ clip, zIndex }) => {\n\t\tconst anim = clipAnimStyle(clip, t, project.fps);\n\t\treturn isMediaClip(clip)\n\t\t\t? `<img src=\"${clip.url}\" style=\"z-index:${zIndex}; ${toCssText(mediaClipCss(anim))}\">`\n\t\t\t: `<div style=\"z-index:${zIndex}; ${toCssText(textClipCss(clip.style, stage.clientHeight, anim))}\">${clip.text}</div>`;\n\t});\n\tstage.innerHTML = html.join('');\n}\n```\n\n### Using Remotion specifically\n\nRemotion works, but it's **host-owned glue** — the library gives you the seam and the data, not\na drop-in Remotion player. There are two things to bridge:\n\n1. **React ↔ Svelte.** Remotion's `<Player>` is a React component and this is Svelte 5, so you\n   mount a small React island (`createRoot`) inside a Svelte wrapper.\n2. **Playback.** The built-in `PlaybackEngine` only drives the native `<video>`/`<audio>`\n   elements in `PreviewStage` — it does **not** drive Remotion. You sync the editor store and the\n   Player's `playerRef` to each other yourself, in **both** directions.\n\nThe store API you wire against (all reactive, exported types):\n\n| Read from editor  | Meaning                         | Call on editor (when user scrubs _inside_ the Player)      |\n| ----------------- | ------------------------------- | ---------------------------------------------------------- |\n| `editor.playhead` | current time, **float seconds** | `editor.seek(seconds)` / `editor.seekFrame(frame)`         |\n| `editor.playing`  | boolean                         | `editor.play()` / `editor.pause()` / `editor.togglePlay()` |\n| `editor.fps`      | project fps (int)               | —                                                          |\n| `editor.project`  | full `TimelineProject`          | —                                                          |\n\n#### Bridge component (`RemotionPreview.svelte`)\n\n```svelte\n<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { createRoot, type Root } from 'react-dom/client';\n\timport { createElement } from 'react';\n\timport { Player, type PlayerRef } from '@remotion/player';\n\timport { compositionSize, secToFrame, type SectionCtx } from '@ariefsn/svelte-video-editor';\n\timport { MyComposition } from './MyComposition'; // your Remotion composition (React)\n\n\t// Passed straight from the preview snippet: <RemotionPreview {ctx} />\n\tlet { ctx }: { ctx: SectionCtx } = $props();\n\tconst editor = ctx.editor;\n\n\tlet mountEl: HTMLDivElement;\n\tlet root: Root;\n\tlet player: PlayerRef | null = null;\n\t// Suppress the echo when an editor→player seek triggers the player's own event.\n\tlet applying = false;\n\n\t// aspectRatio has no width/height — derive output pixels (16:9 → 1920×1080).\n\tconst dims = $derived(compositionSize(editor.project.aspectRatio));\n\n\tonMount(() => {\n\t\troot = createRoot(mountEl);\n\t\trenderPlayer();\n\t});\n\tonDestroy(() => root?.unmount());\n\n\tfunction renderPlayer() {\n\t\troot.render(\n\t\t\tcreateElement(Player, {\n\t\t\t\tref: (r: PlayerRef | null) => (player = r),\n\t\t\t\tcomponent: MyComposition,\n\t\t\t\t// Map clips/tracks + resolveAsset into your composition's props:\n\t\t\t\tinputProps: { project: editor.project, resolveAsset: ctx.host.resolveAsset },\n\t\t\t\tdurationInFrames: Math.max(1, secToFrame(editor.duration, editor.fps)),\n\t\t\t\tcompositionWidth: dims.width,\n\t\t\t\tcompositionHeight: dims.height,\n\t\t\t\tfps: editor.fps,\n\t\t\t\tcontrols: false, // the editor's TransportBar already drives playback\n\t\t\t\tstyle: { width: '100%', height: '100%' }\n\t\t\t})\n\t\t);\n\t}\n\n\t// Re-render the Player when the timeline/aspect changes.\n\t$effect(() => {\n\t\tvoid editor.project; // re-run on project edits\n\t\tif (root) renderPlayer();\n\t});\n\n\t// editor → player: keep the Player's frame + play state in sync with the store.\n\t$effect(() => {\n\t\tif (!player) return;\n\t\tconst frame = secToFrame(editor.playhead, editor.fps);\n\t\tif (player.getCurrentFrame() !== frame) {\n\t\t\tapplying = true;\n\t\t\tplayer.seekTo(frame);\n\t\t\tapplying = false;\n\t\t}\n\t\tif (editor.playing && player.isPlaying() === false) player.play();\n\t\tif (!editor.playing && player.isPlaying()) player.pause();\n\t});\n\n\t// player → editor: when the user scrubs/plays inside the Player, push it back.\n\t$effect(() => {\n\t\tif (!player) return;\n\t\tconst onSeek = (e: { detail: { frame: number } }) => {\n\t\t\tif (applying) return; // ignore our own seekTo\n\t\t\teditor.seekFrame(e.detail.frame);\n\t\t};\n\t\tconst onPlay = () => editor.play();\n\t\tconst onPause = () => editor.pause();\n\t\tplayer.addEventListener('seeked', onSeek);\n\t\tplayer.addEventListener('play', onPlay);\n\t\tplayer.addEventListener('pause', onPause);\n\t\treturn () => {\n\t\t\tplayer?.removeEventListener('seeked', onSeek);\n\t\t\tplayer?.removeEventListener('play', onPlay);\n\t\t\tplayer?.removeEventListener('pause', onPause);\n\t\t};\n\t});\n</script>\n\n<div bind:this={mountEl} class=\"h-full w-full\"></div>\n```\n\nThen drop it into the `preview` snippet:\n\n```svelte\n<TimelineEditor {project} {onChange} {resolveAsset} {generateThumbnail} {onExport} {can}>\n\t{#snippet preview(ctx)}\n\t\t<RemotionPreview {ctx} />\n\t{/snippet}\n</TimelineEditor>\n```\n\n> **The `applying` guard matters.** Without it, an editor→player `seekTo` fires the Player's\n> `seeked` event, which calls `editor.seekFrame`, which re-runs the editor→player effect — an\n> infinite ping-pong. Gate one direction (here, the player→editor push ignores seeks we\n> initiated).\n\n#### Mapping the timeline into a composition\n\nInside `MyComposition` (your React code), import from `@ariefsn/svelte-video-editor/pure` and let\nthe helpers decide layering, timing, styles and audio — the result matches the editor preview:\n\n```tsx\nimport { AbsoluteFill, Audio, Img, OffthreadVideo, Sequence, useCurrentFrame } from 'remotion';\nimport {\n\tbackgroundCss,\n\tclipAnimStyle,\n\tclipAudible,\n\tclipGain,\n\tcompositionSize,\n\tisMediaClip,\n\tlayeredClips,\n\tmediaClipCss,\n\ttextClipCss,\n\ttype TimelineProject\n} from '@ariefsn/svelte-video-editor/pure';\n\nexport function MyComposition({\n\tproject,\n\turls\n}: {\n\tproject: TimelineProject;\n\turls: Record<string, string>;\n}) {\n\tconst frame = useCurrentFrame();\n\tconst { fps } = project;\n\tconst t = frame / fps;\n\tconst { height } = compositionSize(project.aspectRatio);\n\n\treturn (\n\t\t<AbsoluteFill style={{ background: backgroundCss(project.background) }}>\n\t\t\t{layeredClips(project).map(({ clip, zIndex }) => {\n\t\t\t\tconst anim = clipAnimStyle(clip, t, fps);\n\t\t\t\treturn (\n\t\t\t\t\t<Sequence key={clip.id} from={clip.startF} durationInFrames={clip.durationF}>\n\t\t\t\t\t\t{!isMediaClip(clip) ? (\n\t\t\t\t\t\t\t<div style={{ zIndex, ...textClipCss(clip.style, height, anim) }}>{clip.text}</div>\n\t\t\t\t\t\t) : clip.kind === 'image' ? (\n\t\t\t\t\t\t\t<Img src={urls[clip.id]} style={{ zIndex, ...mediaClipCss(anim) }} />\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\t\tconst Media = clip.kind === 'video' ? OffthreadVideo : Audio;\n\t\t\t\t\t\t\t\tconst audible = clipAudible(project, clip);\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Media\n\t\t\t\t\t\t\t\t\t\tsrc={urls[clip.id]}\n\t\t\t\t\t\t\t\t\t\tstartFrom={clip.trimInF} // `trimBefore` on newer Remotion\n\t\t\t\t\t\t\t\t\t\t// f is relative to the Sequence start\n\t\t\t\t\t\t\t\t\t\tvolume={(f) => (audible ? clipGain(clip, (clip.startF + f) / fps, fps) : 0)}\n\t\t\t\t\t\t\t\t\t\tstyle={{ zIndex, ...mediaClipCss(anim) }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})()\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Sequence>\n\t\t\t\t);\n\t\t\t})}\n\t\t</AbsoluteFill>\n\t);\n}\n```\n\nResolve media URLs (`resolveAsset(clip.assetId)`, falling back to `clip.url`) before rendering\nand pass them in via `inputProps`.\n\n> **Simpler path:** if you only want Remotion for high-fidelity _output_ (not live scrubbing),\n> skip the `preview` snippet entirely and keep the built-in `PreviewStage`. Just feed `project`\n> into a Remotion render inside `onExport(project, range?)` — no React island, no sync glue.\n\n## Localization\n\nThere is no `locale` prop — pass a fully-translated `messages` map. **`messages` is fully typed**, so\nyou get autocomplete for every key and a compile error on typos:\n\n```ts\nimport type { Messages } from '@ariefsn/svelte-video-editor';\nimport { defaultMessages } from '@ariefsn/svelte-video-editor';\n\n// Partial override — every key autocompletes:\nconst messages: Partial<Messages> = {\n\texport: 'Render',\n\tsplit: 'Cut',\n\t// most keys are strings; a few interpolate and are functions:\n\tdelete_clips_confirm: ({ count }) => `Delete ${count} clip(s)?`\n};\n```\n\n```svelte\n<!-- `messages` is typed as Partial<Messages> -> IntelliSense lists all keys -->\n<TimelineEditor messages={{ export: 'Render', split: 'Cut' }} … />\n```\n\nExports for typing/translating:\n\n- **`Messages`** — the full message map type (all keys; strings + the 4 interpolating functions).\n- **`MessagesOverride`** = `Partial<Messages>` — the type of the `messages` prop.\n- **`MessageKey`** — a union of every key name.\n- **`defaultMessages`** — the English defaults (clone it as a base for a full translation).\n\nSwap the whole `messages` object at runtime to switch languages reactively.\n\n---\n\n## License\n\nMIT © [Arief Setiyo Nugroho](https://github.com/ariefsn) — [me@ariefsn.dev](mailto:me@ariefsn.dev)\n\nSee [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}