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:
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
exportconstANIMATION_PROPERTY_PATHS=[
// ...existing paths
"background.paddingX",
]asconst;
```
### 2. Add a registry entry — `src/lib/animation/property-registry.ts`
-`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`:
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:
**For color fields**, use `useKeyframedColorProperty` instead. It returns `{ onChange, onChangeEnd, toggleKeyframe, isKeyframedAtTime }` — wire `onChange({ color })` and `onChangeEnd` directly to the `ColorPicker`.