summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/actions.md170
-rw-r--r--docs/countries-search.md102
-rw-r--r--docs/keyframes.md408
3 files changed, 340 insertions, 340 deletions
diff --git a/docs/actions.md b/docs/actions.md
index 44c2153f..3d001e86 100644
--- a/docs/actions.md
+++ b/docs/actions.md
@@ -1,85 +1,85 @@
-# Actions System
-
-Actions are the trigger layer for user-initiated operations. They connect keyboard shortcuts, UI buttons, and context menus to editor functionality.
-
-## Adding a New Action
-
-### 1. Define the action — `src/lib/actions/definitions.ts`
-
-Add an entry to the `ACTIONS` object:
-
-```typescript
-"my-action": {
- description: "What it does",
- category: "editing", // playback | navigation | editing | selection | history | timeline | controls
- defaultShortcuts: ["ctrl+m"], // optional
- args: { someValue: "number" }, // optional, only if it takes args
-},
-```
-
-**If your shortcut uses a special key** (not a letter/digit), check `getPressedKey` in `src/stores/keybindings-store.ts` and add a case if it's missing:
-
-```typescript
-if (key === "escape") return "escape";
-```
-
-**If your action has a `defaultShortcuts`**, also add a keybindings migration so existing users get it (keybindings are persisted in localStorage — new defaults only apply to fresh installs):
-
-1. Create `src/stores/keybindings/migrations/vN-to-vN+1.ts`:
-
-```typescript
-export function vNToVN1({ state }: { state: unknown }): unknown {
- const s = state as { keybindings: Record<string, string>; isCustomized: boolean };
- const keybindings = { ...s.keybindings };
- if (!keybindings["my-key"]) {
- keybindings["my-key"] = "my-action";
- }
- return { ...s, keybindings };
-}
-```
-
-2. Register it in `src/stores/keybindings/migrations/index.ts` and bump `CURRENT_VERSION`.
-
-### 2. Register the handler — `src/hooks/actions/use-editor-actions.ts`
-
-```typescript
-useActionHandler(
- "my-action",
- () => {
- editor.timeline.doSomething();
- },
- undefined, // isActive: MutableRefObject<boolean> | boolean | undefined
-);
-```
-
-### 3. Register arg types (if needed) — `src/lib/actions/types.ts`
-
-Only required if your action accepts arguments:
-
-```typescript
-export type TActionArgsMap = {
- // ...existing actions...
- "my-action": { someValue: number } | undefined; // | undefined = optional args
-};
-```
-
-## Invoking Actions
-
-Use `invokeAction` for any user-triggered operation (buttons, context menus, etc.):
-
-```typescript
-import { invokeAction } from "@/lib/actions";
-
-invokeAction("my-action");
-invokeAction("seek-forward", { seconds: 5 });
-```
-
-Avoid calling `editor.xxx()` directly from UI components — that bypasses the action layer (toasts, validation feedback, keybinding support).
-
-## The `isActive` parameter
-
-The third argument to `useActionHandler` controls when the handler is active:
-
-- `undefined` — always active
-- `true` / `false` — statically enabled/disabled
-- `MutableRefObject<boolean>` — reactive, toggled at runtime (e.g. only active when a panel is focused)
+# Actions System
+
+Actions are the trigger layer for user-initiated operations. They connect keyboard shortcuts, UI buttons, and context menus to editor functionality.
+
+## Adding a New Action
+
+### 1. Define the action — `src/lib/actions/definitions.ts`
+
+Add an entry to the `ACTIONS` object:
+
+```typescript
+"my-action": {
+ description: "What it does",
+ category: "editing", // playback | navigation | editing | selection | history | timeline | controls
+ defaultShortcuts: ["ctrl+m"], // optional
+ args: { someValue: "number" }, // optional, only if it takes args
+},
+```
+
+**If your shortcut uses a special key** (not a letter/digit), check `getPressedKey` in `src/stores/keybindings-store.ts` and add a case if it's missing:
+
+```typescript
+if (key === "escape") return "escape";
+```
+
+**If your action has a `defaultShortcuts`**, also add a keybindings migration so existing users get it (keybindings are persisted in localStorage — new defaults only apply to fresh installs):
+
+1. Create `src/stores/keybindings/migrations/vN-to-vN+1.ts`:
+
+```typescript
+export function vNToVN1({ state }: { state: unknown }): unknown {
+ const s = state as { keybindings: Record<string, string>; isCustomized: boolean };
+ const keybindings = { ...s.keybindings };
+ if (!keybindings["my-key"]) {
+ keybindings["my-key"] = "my-action";
+ }
+ return { ...s, keybindings };
+}
+```
+
+2. Register it in `src/stores/keybindings/migrations/index.ts` and bump `CURRENT_VERSION`.
+
+### 2. Register the handler — `src/hooks/actions/use-editor-actions.ts`
+
+```typescript
+useActionHandler(
+ "my-action",
+ () => {
+ editor.timeline.doSomething();
+ },
+ undefined, // isActive: MutableRefObject<boolean> | boolean | undefined
+);
+```
+
+### 3. Register arg types (if needed) — `src/lib/actions/types.ts`
+
+Only required if your action accepts arguments:
+
+```typescript
+export type TActionArgsMap = {
+ // ...existing actions...
+ "my-action": { someValue: number } | undefined; // | undefined = optional args
+};
+```
+
+## Invoking Actions
+
+Use `invokeAction` for any user-triggered operation (buttons, context menus, etc.):
+
+```typescript
+import { invokeAction } from "@/lib/actions";
+
+invokeAction("my-action");
+invokeAction("seek-forward", { seconds: 5 });
+```
+
+Avoid calling `editor.xxx()` directly from UI components — that bypasses the action layer (toasts, validation feedback, keybinding support).
+
+## The `isActive` parameter
+
+The third argument to `useActionHandler` controls when the handler is active:
+
+- `undefined` — always active
+- `true` / `false` — statically enabled/disabled
+- `MutableRefObject<boolean>` — reactive, toggled at runtime (e.g. only active when a panel is focused)
diff --git a/docs/countries-search.md b/docs/countries-search.md
index 62ed3913..8bce73b4 100644
--- a/docs/countries-search.md
+++ b/docs/countries-search.md
@@ -1,51 +1,51 @@
-# Countries search — Data Guide
-
-## Structure
-
-Each country entry in `apps/web/public/countries.json` follows this shape:
-
-```json
-{
- "name": "Denmark",
- "code": "DK",
- "languages": ["danish"],
- "flag_colors": ["red", "white"],
- "region": "Northern Europe"
-}
-```
-
-### name
-Use the commonly recognized country or territory name.
-
-### code
-Use the canonical country/territory code.
-
-### flag_colors
-Canonical color names only: `red`, `white`, `blue`, `green`, `yellow`, `black`, `orange`, `purple`.
-
-### languages
-The official or widely spoken languages.
-
-### region
-Consistent values — don't mix "Europe" and "Western Europe" for the same type of country.
-
-| Region | Examples |
-|---|---|
-| Northern Europe | Denmark, Sweden, Norway, Finland, Iceland |
-| Western Europe | France, Germany, Switzerland, UK, Netherlands |
-| Southern Europe | Italy, Spain, Greece, Portugal |
-| Eastern Europe | Poland, Russia, Czech Republic, Hungary |
-| Middle East | Saudi Arabia, UAE, Israel, Turkey |
-| East Asia | Japan, China, South Korea |
-| Southeast Asia | Thailand, Vietnam, Indonesia, Singapore |
-| South Asia | India, Pakistan, Bangladesh |
-| Central Asia | Kazakhstan, Uzbekistan |
-| North Africa | Egypt, Morocco, Tunisia |
-| Sub-Saharan Africa | Nigeria, Kenya, South Africa, Ethiopia |
-| North America | USA, Canada, Mexico |
-| Central America | Panama, Costa Rica, Guatemala |
-| Caribbean | Cuba, Bahamas, Jamaica |
-| South America | Brazil, Argentina, Colombia |
-| Oceania | Australia, New Zealand |
-| Atlantic Ocean | Ascension Island, Saint Helena |
-| North Atlantic | Bermuda |
+# Countries search — Data Guide
+
+## Structure
+
+Each country entry in `apps/web/public/countries.json` follows this shape:
+
+```json
+{
+ "name": "Denmark",
+ "code": "DK",
+ "languages": ["danish"],
+ "flag_colors": ["red", "white"],
+ "region": "Northern Europe"
+}
+```
+
+### name
+Use the commonly recognized country or territory name.
+
+### code
+Use the canonical country/territory code.
+
+### flag_colors
+Canonical color names only: `red`, `white`, `blue`, `green`, `yellow`, `black`, `orange`, `purple`.
+
+### languages
+The official or widely spoken languages.
+
+### region
+Consistent values — don't mix "Europe" and "Western Europe" for the same type of country.
+
+| Region | Examples |
+|---|---|
+| Northern Europe | Denmark, Sweden, Norway, Finland, Iceland |
+| Western Europe | France, Germany, Switzerland, UK, Netherlands |
+| Southern Europe | Italy, Spain, Greece, Portugal |
+| Eastern Europe | Poland, Russia, Czech Republic, Hungary |
+| Middle East | Saudi Arabia, UAE, Israel, Turkey |
+| East Asia | Japan, China, South Korea |
+| Southeast Asia | Thailand, Vietnam, Indonesia, Singapore |
+| South Asia | India, Pakistan, Bangladesh |
+| Central Asia | Kazakhstan, Uzbekistan |
+| North Africa | Egypt, Morocco, Tunisia |
+| Sub-Saharan Africa | Nigeria, Kenya, South Africa, Ethiopia |
+| North America | USA, Canada, Mexico |
+| Central America | Panama, Costa Rica, Guatemala |
+| Caribbean | Cuba, Bahamas, Jamaica |
+| South America | Brazil, Argentina, Colombia |
+| Oceania | Australia, New Zealand |
+| Atlantic Ocean | Ascension Island, Saint Helena |
+| North Atlantic | Bermuda |
diff --git a/docs/keyframes.md b/docs/keyframes.md
index 97619626..afa85846 100644
--- a/docs/keyframes.md
+++ b/docs/keyframes.md
@@ -1,204 +1,204 @@
-# Keyframe System
-
-Keyframes allow element properties to change over time. The system is split into three layers: the **data model** (how keyframes are stored), the **registry** (which properties support keyframes and how to read/write them), and the **UI** (hooks and components that wire it all together).
-
-## How It Works
-
-### Data model
-
-Every `BaseTimelineElement` has an optional `animations?: ElementAnimations` field:
-
-```typescript
-interface ElementAnimations {
- channels: Record<string, AnimationChannel | undefined>;
-}
-```
-
-A channel is a typed bucket of keyframes keyed by property path (e.g. `"opacity"`, `"background.color"`). Three channel types exist: `NumberAnimationChannel`, `ColorAnimationChannel`, and `DiscreteAnimationChannel`.
-
-### Registry
-
-`src/lib/animation/property-registry.ts` defines which property paths are animatable and how to read/write their values on an element. `src/types/animation.ts` holds the canonical list of valid paths in `ANIMATION_PROPERTY_PATHS`.
-
-### Resolver
-
-`src/lib/animation/resolve.ts` provides functions that return the effective value of a property at a given local time — falling back to the element's static value when no keyframes exist.
-
-### Renderer
-
-Nodes in `src/services/renderer/` call the resolve functions before drawing so that animated properties interpolate correctly during export and preview.
-
-### UI
-
-Two hooks in `src/components/editor/panels/properties/hooks/` handle the keyframe-aware field logic:
-
-- `useKeyframedNumberProperty` — for numeric fields (opacity, position, scale, etc.)
-- `useKeyframedColorProperty` — for color pickers
-
-Both hooks handle the toggle/add/remove keyframe flow and automatically switch between writing to the static property and writing to the animation channel depending on whether keyframes are active.
-
----
-
-## Adding a New Animatable Property
-
-Using `"background.paddingX"` as an example.
-
-### 1. Register the path — `src/types/animation.ts`
-
-```typescript
-export const ANIMATION_PROPERTY_PATHS = [
- // ...existing paths
- "background.paddingX",
-] as const;
-```
-
-### 2. Add a registry entry — `src/lib/animation/property-registry.ts`
-
-```typescript
-"background.paddingX": {
- valueKind: "number", // "number" | "color" | "discrete"
- defaultInterpolation: "linear",
- numericRange: { min: 0 }, // optional, only for number properties
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? { ...element, background: { ...element.background, paddingX: value as number } }
- : element,
-},
-```
-
-**Notes:**
-- `getValue` must return the effective value including any defaults — this is what gets recorded when a keyframe is added.
-- `setValue` receives `AnimationValue` (`number | string | boolean`). Cast to the correct type since `coerceAnimationValueForProperty` already validated it upstream.
-- For color properties, use `valueKind: "color"` and cast `value as string`.
-
-### 3. Add a resolve function — `src/lib/animation/resolve.ts`
-
-For **numbers**, use the existing generic `resolveNumberAtTime`:
-
-```typescript
-import { resolveNumberAtTime } from "@/lib/animation";
-
-const resolvedPaddingX = resolveNumberAtTime({
- baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- animations: element.animations,
- propertyPath: "background.paddingX",
- localTime,
-});
-```
-
-For **colors**, use `resolveColorAtTime`:
-
-```typescript
-const resolvedColor = resolveColorAtTime({
- baseColor: element.color,
- animations: element.animations,
- propertyPath: "color",
- localTime,
-});
-```
-
-If neither fits (new value kind), add a dedicated resolve function following the same pattern as `resolveOpacityAtTime` and export it from `src/lib/animation/index.ts`.
-
-### 4. Wire the renderer
-
-In the relevant node (`src/services/renderer/nodes/`), call the resolve function before drawing:
-
-```typescript
-const resolvedPaddingX = resolveNumberAtTime({
- baseValue: this.params.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- animations: this.params.animations,
- propertyPath: "background.paddingX",
- localTime,
-});
-```
-
-Use the resolved value (not `this.params.*`) anywhere that value affects rendering.
-
-### 5. Wire the UI
-
-In the properties panel, replace `usePropertyDraft` with the appropriate keyframe hook and add a `KeyframeToggle` to the field.
-
-**For number fields:**
-
-```typescript
-const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
- startTime: element.startTime,
- duration: element.duration,
-});
-
-const resolvedPaddingX = resolveNumberAtTime({
- baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- animations: element.animations,
- propertyPath: "background.paddingX",
- localTime,
-});
-
-const paddingX = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.paddingX",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedPaddingX).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
- },
- valueAtPlayhead: resolvedPaddingX,
- buildBaseUpdates: ({ value }) => ({
- background: { ...element.background, paddingX: value },
- }),
-});
-```
-
-In JSX:
-
-```tsx
-<SectionField
- label="Width"
- beforeLabel={
- <KeyframeToggle
- isActive={paddingX.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle background width keyframe"
- onToggle={paddingX.toggleKeyframe}
- />
- }
->
- <NumberField
- value={paddingX.displayValue}
- onFocus={paddingX.onFocus}
- onChange={paddingX.onChange}
- onBlur={paddingX.onBlur}
- onScrub={paddingX.scrubTo}
- onScrubEnd={paddingX.commitScrub}
- onReset={() => paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedPaddingX,
- staticValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX,
- })}
- />
-</SectionField>
-```
-
-**For color fields**, use `useKeyframedColorProperty` instead. It returns `{ onChange, onChangeEnd, toggleKeyframe, isKeyframedAtTime }` — wire `onChange({ color })` and `onChangeEnd` directly to the `ColorPicker`.
-
----
-
-## Checklist
-
-- [ ] Path added to `ANIMATION_PROPERTY_PATHS`
-- [ ] Registry entry added with correct `valueKind`, `supportsElement`, `getValue`, `setValue`
-- [ ] Resolve call added in the renderer node
-- [ ] UI field uses `useKeyframedNumberProperty` or `useKeyframedColorProperty` (not `usePropertyDraft`)
-- [ ] `KeyframeToggle` added to the `SectionField`
-- [ ] `onReset` calls `commitValue` (not `editor.timeline.updateElements` directly)
+# Keyframe System
+
+Keyframes allow element properties to change over time. The system is split into three layers: the **data model** (how keyframes are stored), the **registry** (which properties support keyframes and how to read/write them), and the **UI** (hooks and components that wire it all together).
+
+## How It Works
+
+### Data model
+
+Every `BaseTimelineElement` has an optional `animations?: ElementAnimations` field:
+
+```typescript
+interface ElementAnimations {
+ channels: Record<string, AnimationChannel | undefined>;
+}
+```
+
+A channel is a typed bucket of keyframes keyed by property path (e.g. `"opacity"`, `"background.color"`). Three channel types exist: `NumberAnimationChannel`, `ColorAnimationChannel`, and `DiscreteAnimationChannel`.
+
+### Registry
+
+`src/lib/animation/property-registry.ts` defines which property paths are animatable and how to read/write their values on an element. `src/types/animation.ts` holds the canonical list of valid paths in `ANIMATION_PROPERTY_PATHS`.
+
+### Resolver
+
+`src/lib/animation/resolve.ts` provides functions that return the effective value of a property at a given local time — falling back to the element's static value when no keyframes exist.
+
+### Renderer
+
+Nodes in `src/services/renderer/` call the resolve functions before drawing so that animated properties interpolate correctly during export and preview.
+
+### UI
+
+Two hooks in `src/components/editor/panels/properties/hooks/` handle the keyframe-aware field logic:
+
+- `useKeyframedNumberProperty` — for numeric fields (opacity, position, scale, etc.)
+- `useKeyframedColorProperty` — for color pickers
+
+Both hooks handle the toggle/add/remove keyframe flow and automatically switch between writing to the static property and writing to the animation channel depending on whether keyframes are active.
+
+---
+
+## Adding a New Animatable Property
+
+Using `"background.paddingX"` as an example.
+
+### 1. Register the path — `src/types/animation.ts`
+
+```typescript
+export const ANIMATION_PROPERTY_PATHS = [
+ // ...existing paths
+ "background.paddingX",
+] as const;
+```
+
+### 2. Add a registry entry — `src/lib/animation/property-registry.ts`
+
+```typescript
+"background.paddingX": {
+ valueKind: "number", // "number" | "color" | "discrete"
+ defaultInterpolation: "linear",
+ numericRange: { min: 0 }, // optional, only for number properties
+ supportsElement: ({ element }) => element.type === "text",
+ getValue: ({ element }) =>
+ element.type === "text"
+ ? (element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX)
+ : null,
+ setValue: ({ element, value }) =>
+ element.type === "text"
+ ? { ...element, background: { ...element.background, paddingX: value as number } }
+ : element,
+},
+```
+
+**Notes:**
+- `getValue` must return the effective value including any defaults — this is what gets recorded when a keyframe is added.
+- `setValue` receives `AnimationValue` (`number | string | boolean`). Cast to the correct type since `coerceAnimationValueForProperty` already validated it upstream.
+- For color properties, use `valueKind: "color"` and cast `value as string`.
+
+### 3. Add a resolve function — `src/lib/animation/resolve.ts`
+
+For **numbers**, use the existing generic `resolveNumberAtTime`:
+
+```typescript
+import { resolveNumberAtTime } from "@/lib/animation";
+
+const resolvedPaddingX = resolveNumberAtTime({
+ baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ animations: element.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+});
+```
+
+For **colors**, use `resolveColorAtTime`:
+
+```typescript
+const resolvedColor = resolveColorAtTime({
+ baseColor: element.color,
+ animations: element.animations,
+ propertyPath: "color",
+ localTime,
+});
+```
+
+If neither fits (new value kind), add a dedicated resolve function following the same pattern as `resolveOpacityAtTime` and export it from `src/lib/animation/index.ts`.
+
+### 4. Wire the renderer
+
+In the relevant node (`src/services/renderer/nodes/`), call the resolve function before drawing:
+
+```typescript
+const resolvedPaddingX = resolveNumberAtTime({
+ baseValue: this.params.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ animations: this.params.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+});
+```
+
+Use the resolved value (not `this.params.*`) anywhere that value affects rendering.
+
+### 5. Wire the UI
+
+In the properties panel, replace `usePropertyDraft` with the appropriate keyframe hook and add a `KeyframeToggle` to the field.
+
+**For number fields:**
+
+```typescript
+const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
+ startTime: element.startTime,
+ duration: element.duration,
+});
+
+const resolvedPaddingX = resolveNumberAtTime({
+ baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ animations: element.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+});
+
+const paddingX = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedPaddingX).toString(),
+ parse: (input) => {
+ const parsed = parseFloat(input);
+ return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
+ },
+ valueAtPlayhead: resolvedPaddingX,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...element.background, paddingX: value },
+ }),
+});
+```
+
+In JSX:
+
+```tsx
+<SectionField
+ label="Width"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={paddingX.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle background width keyframe"
+ onToggle={paddingX.toggleKeyframe}
+ />
+ }
+>
+ <NumberField
+ value={paddingX.displayValue}
+ onFocus={paddingX.onFocus}
+ onChange={paddingX.onChange}
+ onBlur={paddingX.onBlur}
+ onScrub={paddingX.scrubTo}
+ onScrubEnd={paddingX.commitScrub}
+ onReset={() => paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedPaddingX,
+ staticValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX,
+ })}
+ />
+</SectionField>
+```
+
+**For color fields**, use `useKeyframedColorProperty` instead. It returns `{ onChange, onChangeEnd, toggleKeyframe, isKeyframedAtTime }` — wire `onChange({ color })` and `onChangeEnd` directly to the `ColorPicker`.
+
+---
+
+## Checklist
+
+- [ ] Path added to `ANIMATION_PROPERTY_PATHS`
+- [ ] Registry entry added with correct `valueKind`, `supportsElement`, `getValue`, `setValue`
+- [ ] Resolve call added in the renderer node
+- [ ] UI field uses `useKeyframedNumberProperty` or `useKeyframedColorProperty` (not `usePropertyDraft`)
+- [ ] `KeyframeToggle` added to the `SectionField`
+- [ ] `onReset` calls `commitValue` (not `editor.timeline.updateElements` directly)