Context store
The React contexts the provider sets up — Store, Config, Icon, Plugin Registry, Portal — and the hooks that read them.
VideoStudioProvider is a stack of React Context providers. Each one exposes a different runtime
service to the tree below it. Knowing which context holds what helps you (a) reach the right hook and
(b) understand the provider order if you ever need to compose pieces yourself.
Provider stack
The provider wires these in order, outside-in:
LicenseProvider
└── StudioConfigContext ← logo, callbacks, sidebar buttons, masking, routes
└── StudioPortalContext ← root element for portaled popovers
└── IconProvider ← user-level icon overrides
└── StoreProvider ← the single Zustand store
└── PluginRegistryProvider
├── (plugins register here)
└── IconContext (re-applied with merged map) ← defaults + plugin icons + user icons
└── your childrenThe two IconContext layers cooperate: the outer one runs before plugins so plugins see the
user's overrides during onRegister. The inner one runs after plugins so consumers see the
merged final map.
The five contexts
StoreContext (Zustand)
Holds the single store instance. Exposed through two hooks (covered in Store):
import { useStudioStore, useStudioStoreApi } from '@studio-dev/vsdk'
const value = useStudioStore((s) => s.player.currentFrame)
const storeApi = useStudioStoreApi()If you need raw access (you usually don't), import the context itself:
import { StoreContext } from '@studio-dev/vsdk'
const store = useContext(StoreContext)Both useStudioStore and useStudioStoreApi throw if used outside the provider — useful for catching
mounting bugs early.
StudioConfigContext
Holds non-state configuration the consumer passed to the provider: logo, onSave, onExport,
onExportAbort, masking, routes, sidebarButtons, defaultActivePanel.
import { useStudioConfig, useMaskingConfig } from '@studio-dev/vsdk/ui'
function ToolbarSaveButton() {
const { onSave } = useStudioConfig()
// call onSave?.(bundle) ...
}useMaskingConfig() returns the resolved masking config (with defaults applied) — use it from any
masking-aware UI rather than reading the raw context yourself.
IconContext
Holds the resolved StudioIconMap. Read through the icon hooks:
import { useIconMap, useStudioIcon, StudioIcon } from '@studio-dev/vsdk'
const icons = useIconMap() // full map
const PlayIcon = useStudioIcon('play') // single
<StudioIcon name="pause" className="size-4" />The map is recomputed when either icons (consumer overrides) or pluginIcons (registered by
plugins) changes, in that precedence order. Plugins reading ctx.icons during onRegister see the
defaults + user overrides — plugin-registered icons are merged in after.
StudioPortalContext
Holds a reference to the provider's root <div>. Used internally so popovers, tooltips, dropdowns,
and dialogs portal inside the editor rather than the document body — important when the editor is
inside a CSS-isolated container or themed root.
If you build a custom popover/dialog from primitives, you usually don't need to read this — the
@studio-dev/vsdk-ui components already do.
PluginRegistryContext
Holds the live PluginRegistry. Exposed through hooks:
import {
usePluginRegistry,
usePanels,
useInspectorSections,
useInspectorTabs,
useToolbarActions,
useContextMenuActions,
usePluginHotkeys,
} from '@studio-dev/vsdk'| Hook | Returns |
|---|---|
usePluginRegistry() | The raw registry object |
usePanels() | All registered panels, sorted by order |
useInspectorSections(itemType) | Inline sections for an item type |
useInspectorTabs(itemType) | Tab-mode sections for an item type |
useToolbarActions() | Toolbar actions, sorted |
useContextMenuActions() | Context-menu actions, sorted |
usePluginHotkeys() | All plugin-registered hotkeys |
These are what the built-in editor surfaces use to render plugin contributions — so any panel, inspector, toolbar, or context menu you build in your own UI can opt in the same way.
Hooks reference
State / store
| Hook | What it gives you |
|---|---|
useStudioStore(selector) | Reactive read — re-renders on selected value change |
useStudioStoreApi() | Stable store handle — no subscription |
useStudioConfig() | Provider config (logo, onSave, …) |
useMaskingConfig() | Resolved masking config |
Behavior
| Hook | Purpose |
|---|---|
useHotkeys() | Registers the built-in keyboard shortcuts (Space, ⌘Z, Delete, …). Mount once near the editor root |
useFrameSync(playerRef) | Keeps a Remotion player ref in sync with the store's frame |
useCoordinateMap(containerRef) | Screen ↔ composition coordinate transforms |
useItemDrag({ getData }) | Hook to make any element drag a panel item into timeline/player |
License
| Hook | Returns |
|---|---|
useLicense() | License validation state — { status, plan, info, … } |
Render / export
| Hook | Returns |
|---|---|
useRender() | Read-only export state (phase, progress, downloadUrl, …) |
Mounting your own custom editor
StudioEditor is the default shell — full layout with toolbar, sidebar, timeline, inspector. If
you're building a custom layout, the provider stack still applies; you just render different children
underneath:
<VideoStudioProvider plugins={plugins} initialBundle={bundle}>
<YourCustomToolbar />
<YourCustomTimeline />
<YourCustomInspector />
</VideoStudioProvider>Each of your components uses the hooks above to read state, dispatch actions, render plugin panels,
etc. The plugin registry sorts panels and inspector sections for you — you just iterate the array
returned by usePanels() and render each panel's component.
A minimal "render every registered plugin panel" example:
import { usePanels, useIconMap } from '@studio-dev/vsdk'
import { Suspense, useState } from 'react'
function YourCustomSidebar() {
const panels = usePanels()
const [activeId, setActiveId] = useState<string | null>(panels[0]?.id ?? null)
const active = panels.find((p) => p.id === activeId)
const Active = active?.component
return (
<div className="flex h-full">
<nav className="flex flex-col gap-1 p-2 border-r">
{panels.map((p) => {
const Icon = p.icon
return (
<button key={p.id} onClick={() => setActiveId(p.id)} aria-label={p.label}>
<Icon className="size-5" />
</button>
)
})}
</nav>
<div className="flex-1">
{Active && (
<Suspense fallback={null}>
<Active />
</Suspense>
)}
</div>
</div>
)
}This is essentially what the built-in StudioEditor does for the sidebar.
Common mistakes
- Hooks outside the provider —
useStudioStore,useStudioStoreApi,usePluginRegistry, and the icon hooks all require the provider tree. Mount tests with the provider, even when testing a leaf component. - New
pluginsarray every render — the registry re-runsonRegisteron identity change. Memoize. - New
iconsobject every render — same problem. Define outside the component oruseMemo. - Subscribing inside a render-tight selector —
useStudioStore((s) => s)causes re-renders on every state change. Project to the narrowest piece of state you actually need.