Video Studio SDKv0.0.3
Core

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 children

The 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'
HookReturns
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

HookWhat 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

HookPurpose
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

HookReturns
useLicense()License validation state — { status, plan, info, … }

Render / export

HookReturns
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 provideruseStudioStore, useStudioStoreApi, usePluginRegistry, and the icon hooks all require the provider tree. Mount tests with the provider, even when testing a leaf component.
  • New plugins array every render — the registry re-runs onRegister on identity change. Memoize.
  • New icons object every render — same problem. Define outside the component or useMemo.
  • Subscribing inside a render-tight selectoruseStudioStore((s) => s) causes re-renders on every state change. Project to the narrowest piece of state you actually need.

On this page