Context Menu
A right-click menu panel that opens at the cursor point with no built-in trigger
A state atom representing a context menu panel. Its appearance (opened, closed) is a pure function of the state prop. Pass a state directly or drive it from the Remotion timeline via useCurrentState. The component reads no frame — only the state value you give it. Unlike DropdownMenu, there is no built-in trigger — the context menu opens at the cursor click point and the caller positions it there. The panel grows from its top-left corner (the click point) with opacity, scale, and vertical lift.
Installation
$ pnpm dlx shadcn@latest add @remocn/context-menuInstalling context-menu automatically installs the shared remocn-ui core (lib/remocn-ui/) and the dropdown-menu-item atom via registryDependencies (["remocn-ui", "dropdown-menu-item"]). You do not need to install them separately. The panel rows reuse the DropdownMenuItem atom.
States
ContextMenuState is:
type ContextMenuState =
| "opened" // opacity 1, scale 1, translateY 0 — panel fully visible
| "closed" // opacity 0, scale 0.95, translateY -4px — panel hiddenclosed is the default. The panel grows from its top-left corner (transformOrigin: "top left"), which corresponds to the cursor's right-click point. opened reveals the panel; closed hides it.
Snap usage
Pass state directly — the component snaps instantly to that visual. Useful for static previews or when you drive state from your own logic:
import { ContextMenu } from "@/components/remocn/context-menu";
export const Scene = () => (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{/* Position the menu at the right-click point */}
<div style={{ position: "absolute", left: 400, top: 280 }}>
<ContextMenu state="opened" />
</div>
</div>
);To drive state from the timeline, use useCurrentState:
import { useCurrentState } from "@/lib/remocn-ui";
import { ContextMenu } from "@/components/remocn/context-menu";
export const Scene = () => {
const state = useCurrentState(
[
{ at: 44, state: "opened" },
{ at: 92, state: "closed" },
],
"closed",
);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<div style={{ position: "absolute", left: 400, top: 280 }}>
<ContextMenu state={state} />
</div>
</div>
);
};Each at is a Sequence-local authored frame. State persists between steps. See Concepts for the full useCurrentState API.
Smooth transitions
State changes via state snap with no cross-fade. For an animated open/close, use useContextMenuTransition from the copied use-context-menu-transition.ts file. It reads the frame, interpolates between state presets, and returns a resolved ContextMenuStyle — pass it to the style prop:
import { ContextMenu } from "@/components/remocn/context-menu";
import { useContextMenuTransition } from "@/components/remocn/use-context-menu-transition";
export const Scene = () => {
const style = useContextMenuTransition([
{ at: 44, state: "opened", duration: 10 },
{ at: 92, state: "closed", duration: 10 },
]);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<div style={{ position: "absolute", left: 400, top: 280 }}>
<ContextMenu style={style} />
</div>
</div>
);
};The duration field on each step overrides the file's DEFAULT_DURATION (= 10) for that specific transition. To globally tune timing and easing, edit use-context-menu-transition.ts directly in your project — that file is yours (shadcn "own your code" philosophy).
style takes precedence over state when both are provided.
Positioning at the click point
The context menu has no built-in trigger and no x/y props. It renders inside a transparent div whose transformOrigin is "top left" — so the panel grows from whatever corner the caller places it at. Use an absolute wrapper positioned at the cursor's right-click coordinates:
import { Cursor } from "@/components/remocn/cursor";
import { useCursorPath } from "@/components/remocn/use-cursor-path";
import { ContextMenu } from "@/components/remocn/context-menu";
import { useContextMenuTransition } from "@/components/remocn/use-context-menu-transition";
const CLICK_X = 400;
const CLICK_Y = 280;
export const Scene = () => {
const cursorStyle = useCursorPath([
{ at: 0, x: 80, y: 60 },
{ at: 30, x: CLICK_X, y: CLICK_Y, duration: 26 },
{ at: 42, x: CLICK_X, y: CLICK_Y, click: true, duration: 0 },
]);
const menuStyle = useContextMenuTransition([
{ at: 44, state: "opened", duration: 10 },
{ at: 92, state: "closed", duration: 10 },
]);
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{/* Menu top-left corner = cursor click point */}
<div style={{ position: "absolute", left: CLICK_X, top: CLICK_Y }}>
<ContextMenu style={menuStyle} />
</div>
<Cursor style={cursorStyle} variant="pointer" />
</div>
);
};The click: true waypoint on the cursor fires a ripple at that frame — schedule the menu's opened step 1–2 frames after to let the ripple lead the reveal.
Items
The panel renders a column of DropdownMenuItem rows — one per action. The row atom, its states (idle, hover, press), and the useDropdownMenuItemTransition hook are documented under DropdownMenu. When you install context-menu, the dropdown-menu-item atom is automatically pulled in via registryDependencies — no separate install needed.
The ContextMenu container derives each row's state from highlightedIndex and pressedIndex, or you can override a row's visual with itemStyles:
import { useDropdownMenuItemTransition } from "@/components/remocn/use-dropdown-menu-item-transition";
import { useCurrentState } from "@/lib/remocn-ui";
const rowState = useCurrentState(
[
{ at: 60, state: "hover" },
{ at: 72, state: "press" },
{ at: 82, state: "idle" },
],
"idle",
);
const rowStyle = useDropdownMenuItemTransition([{ at: 0, state: rowState }]);
<ContextMenu style={menuStyle} itemStyles={[undefined, rowStyle, undefined, undefined]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
state | "opened" | "closed" | "closed" | Current visual state (snap path). State changes snap — no automatic cross-fade. |
style | ContextMenuStyle | — | Resolved animated visual (smooth path). Pass an interpolated ContextMenuStyle from useContextMenuTransition. Takes precedence over state when provided. |
items | string[] | ["Back", "Reload", "Save As…", "Inspect"] | Array of action labels rendered as DropdownMenuItem rows. |
highlightedIndex | number | -1 | Index of the row under the pointer (hover state). -1 for none. |
pressedIndex | number | -1 | Index of the row being pressed (press state). -1 for none. |
itemStyles | (DropdownMenuItemStyle | undefined)[] | — | Per-row resolved visual override (smooth path). When present it wins over the index-to-state derivation. Drive individual rows with useDropdownMenuItemTransition. |
theme | Partial<RemocnTheme> | — | Per-component theme override. Merges with the active RemocnUIProvider theme and the mode defaults. Must be concrete oklch/hex/rgb values — not CSS custom properties. |
mode | "light" | "dark" | "light" | Sets the base palette. Overrides the mode on RemocnUIProvider when both are present. |
className | string | — | Optional className applied to the panel wrapper element. |