Timeline Events for runtime

Follow on to https://github.com/rive-app/rive/pull/5877! Does similar work for the C++ runtime and reconsiders some naming, will likely need to fix some higher level runtimes @zplata.

I renamed the fired to reportedEvent and added in the time delay too.

Diffs=
f96c86fcc Timeline Events for runtime (#5951)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 290281e..e1220dc 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-853ae7de1d020bc13e84359f816215764923215c
+f96c86fcc800a1fa2bec3e97be00cab395ea838d
diff --git a/dev/core_generator/lib/src/definition.dart b/dev/core_generator/lib/src/definition.dart
index 596b969..6515f77 100644
--- a/dev/core_generator/lib/src/definition.dart
+++ b/dev/core_generator/lib/src/definition.dart
@@ -26,6 +26,9 @@
       .where((property) => property.isRuntime)
       .toList(growable: false);
 
+  Iterable<Property> get storedProperties =>
+      properties.where((property) => property.getExportType().storesData);
+
   Definition? _extensionOf;
   Key? _key;
   bool _isAbstract = false;
@@ -167,13 +170,13 @@
         code.writeln('static const uint16_t ${property.name}PropertyKey = '
             '${property.key!.intValue};');
       }
-      if (properties.any((prop) => !prop.isEncoded)) {
+      if (storedProperties.any((prop) => !prop.isEncoded)) {
         code.writeln('private:');
       }
 
       // Write fields.
       for (final property in properties) {
-        if (property.isEncoded) {
+        if (property.isEncoded || !property.getExportType().storesData) {
           // Encoded properties don't store data, it's up to the implementation
           // to decode and store what it needs.
           continue;
@@ -195,7 +198,13 @@
       // Write getter/setters.
       code.writeln('public:');
       for (final property in properties) {
-        if (property.isEncoded) {
+        if (!property.getExportType().storesData) {
+          code.writeln((property.isSetOverride ? '' : 'virtual ') +
+              'void ${property.name}' +
+              '(const ${property.type.cppName}& value) ' +
+              (property.isSetOverride ? 'override' : '') +
+              '= 0;');
+        } else if (property.isEncoded) {
           // Encoded properties just have a pure virtual decoder that needs to
           // be implemented. Also requires an implemention of copyPropertyName
           // as that will no longer automatically be copied by the generated
@@ -235,9 +244,9 @@
       code.writeln('Core* clone() const override;');
     }
 
-    if (properties.isNotEmpty || _extensionOf == null) {
+    if (storedProperties.isNotEmpty || _extensionOf == null) {
       code.writeln('void copy(const ${_name}Base& object) {');
-      for (final property in properties) {
+      for (final property in storedProperties) {
         if (property.isEncoded) {
           code.writeln('copy${property.capitalizedName}(object);');
         } else {
@@ -255,7 +264,7 @@
       code.writeln('bool deserialize(uint16_t propertyKey, '
           'BinaryReader& reader) override {');
 
-      if (properties.isNotEmpty) {
+      if (storedProperties.isNotEmpty) {
         code.writeln('switch (propertyKey){');
         for (final property in properties) {
           code.writeln('case ${property.name}PropertyKey:');
@@ -279,8 +288,8 @@
     }
 
     code.writeln('protected:');
-    if (properties.isNotEmpty) {
-      for (final property in properties) {
+    if (storedProperties.isNotEmpty) {
+      for (final property in storedProperties) {
         code.writeln('virtual void ${property.name}Changed() {}');
       }
     }
@@ -465,6 +474,9 @@
       ctxCode.writeln('}}');
     }
     for (final fieldType in getSetFieldTypes.keys) {
+      if (!fieldType.storesData) {
+        continue;
+      }
       ctxCode.writeln(
           'static ${fieldType.cppName} get${fieldType.capitalizedName}('
           'Core* object, int propertyKey){');
@@ -488,6 +500,9 @@
     ctxCode.writeln('switch(propertyKey) {');
 
     for (final fieldType in usedFieldTypes.keys) {
+      if (!fieldType.storesData) {
+        continue;
+      }
       var properties = usedFieldTypes[fieldType];
       if (properties != null) {
         for (final property in properties) {
@@ -500,6 +515,28 @@
 
     ctxCode.writeln('default: return -1;}}');
 
+    ctxCode.writeln('''
+      static bool isCallback(uint32_t propertyKey) {
+        switch(propertyKey) {''');
+    for (final fieldType in usedFieldTypes.keys) {
+      var properties = usedFieldTypes[fieldType];
+      if (properties != null) {
+        bool found = false;
+        for (final property in properties) {
+          if (property.getExportType().name == 'callback') {
+            found = true;
+            ctxCode.write('case ${property.definition._name}Base');
+            ctxCode.write('::${property.name}PropertyKey:');
+          }
+        }
+        if (found) {
+          ctxCode.writeln('return true;');
+        }
+      }
+    }
+    ctxCode.writeln('default:return false;');
+    ctxCode.writeln('}}');
+
     ctxCode.writeln('};}');
 
     var output = generatedHppPath;
diff --git a/dev/core_generator/lib/src/field_type.dart b/dev/core_generator/lib/src/field_type.dart
index 60f3025..49edcd0 100644
--- a/dev/core_generator/lib/src/field_type.dart
+++ b/dev/core_generator/lib/src/field_type.dart
@@ -16,11 +16,14 @@
   final String _runtimeCoreType;
   String get runtimeCoreType => _runtimeCoreType;
 
+  final bool storesData;
+
   FieldType(
     this.name,
     this._runtimeCoreType, {
     String? cppName,
     this.include,
+    this.storesData = true,
   }) {
     _cppName = cppName ?? name;
     _types[name] = this;
diff --git a/dev/core_generator/lib/src/field_types/callback_field_type.dart b/dev/core_generator/lib/src/field_types/callback_field_type.dart
new file mode 100644
index 0000000..5c596e2
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/callback_field_type.dart
@@ -0,0 +1,14 @@
+import 'package:core_generator/src/field_type.dart';
+
+class CallbackFieldType extends FieldType {
+  CallbackFieldType()
+      : super(
+          'callback',
+          'CoreCallbackType',
+          cppName: 'CallbackData',
+          storesData: false,
+        );
+
+  @override
+  String get defaultValue => '0';
+}
diff --git a/dev/core_generator/lib/src/field_types/initialize.dart b/dev/core_generator/lib/src/field_types/initialize.dart
index 5d37260..4c94698 100644
--- a/dev/core_generator/lib/src/field_types/initialize.dart
+++ b/dev/core_generator/lib/src/field_types/initialize.dart
@@ -2,6 +2,7 @@
 
 import 'package:core_generator/src/field_type.dart';
 import 'package:core_generator/src/field_types/bytes_field_type.dart';
+import 'package:core_generator/src/field_types/callback_field_type.dart';
 
 late List<FieldType> fields;
 
@@ -13,5 +14,6 @@
     DoubleFieldType(),
     BoolFieldType(),
     ColorFieldType(),
+    CallbackFieldType(),
   ];
 }
diff --git a/dev/defs/animation/interpolating_keyframe.json b/dev/defs/animation/interpolating_keyframe.json
new file mode 100644
index 0000000..ddc5375
--- /dev/null
+++ b/dev/defs/animation/interpolating_keyframe.json
@@ -0,0 +1,31 @@
+{
+  "name": "InterpolatingKeyFrame",
+  "key": {
+    "int": 170,
+    "string": "interpolatingkeyframe"
+  },
+  "abstract": true,
+  "extends": "animation/keyframe.json",
+  "properties": {
+    "interpolationType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 68,
+        "string": "interpolation"
+      },
+      "description": "The type of interpolation index in KeyframeInterpolation applied to this keyframe."
+    },
+    "interpolatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 69,
+        "string": "interpolatorid"
+      },
+      "description": "The id of the custom interpolator used when interpolation is Cubic."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe.json b/dev/defs/animation/keyframe.json
index 1d22274..1384e8d 100644
--- a/dev/defs/animation/keyframe.json
+++ b/dev/defs/animation/keyframe.json
@@ -25,26 +25,6 @@
         "string": "frame"
       },
       "description": "Timecode as frame number can be converted to time by dividing by animation fps."
-    },
-    "interpolationType": {
-      "type": "uint",
-      "initialValue": "0",
-      "key": {
-        "int": 68,
-        "string": "interpolation"
-      },
-      "description": "The type of interpolation index in KeyframeInterpolation applied to this keyframe."
-    },
-    "interpolatorId": {
-      "type": "Id",
-      "typeRuntime": "uint",
-      "initialValue": "Core.missingId",
-      "initialValueRuntime": "-1",
-      "key": {
-        "int": 69,
-        "string": "interpolatorid"
-      },
-      "description": "The id of the custom interpolator used when interpolation is Cubic."
     }
   }
 }
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_bool.json b/dev/defs/animation/keyframe_bool.json
index 7deb70b..bdf1d68 100644
--- a/dev/defs/animation/keyframe_bool.json
+++ b/dev/defs/animation/keyframe_bool.json
@@ -4,7 +4,7 @@
     "int": 84,
     "string": "keyframebool"
   },
-  "extends": "animation/keyframe.json",
+  "extends": "animation/interpolating_keyframe.json",
   "properties": {
     "value": {
       "type": "bool",
diff --git a/dev/defs/animation/keyframe_callback.json b/dev/defs/animation/keyframe_callback.json
new file mode 100644
index 0000000..61e84b5
--- /dev/null
+++ b/dev/defs/animation/keyframe_callback.json
@@ -0,0 +1,8 @@
+{
+  "name": "KeyFrameCallback",
+  "key": {
+    "int": 171,
+    "string": "keyframe_callback"
+  },
+  "extends": "animation/keyframe.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_color.json b/dev/defs/animation/keyframe_color.json
index f13ab7b..c456254 100644
--- a/dev/defs/animation/keyframe_color.json
+++ b/dev/defs/animation/keyframe_color.json
@@ -4,7 +4,7 @@
     "int": 37,
     "string": "keyframecolor"
   },
-  "extends": "animation/keyframe.json",
+  "extends": "animation/interpolating_keyframe.json",
   "properties": {
     "value": {
       "type": "Color",
diff --git a/dev/defs/animation/keyframe_double.json b/dev/defs/animation/keyframe_double.json
index 532976a..4ea3405 100644
--- a/dev/defs/animation/keyframe_double.json
+++ b/dev/defs/animation/keyframe_double.json
@@ -4,7 +4,7 @@
     "int": 30,
     "string": "keyframedouble"
   },
-  "extends": "animation/keyframe.json",
+  "extends": "animation/interpolating_keyframe.json",
   "properties": {
     "value": {
       "type": "double",
diff --git a/dev/defs/animation/keyframe_id.json b/dev/defs/animation/keyframe_id.json
index 421a34b..299be61 100644
--- a/dev/defs/animation/keyframe_id.json
+++ b/dev/defs/animation/keyframe_id.json
@@ -4,7 +4,7 @@
     "int": 50,
     "string": "keyframeid"
   },
-  "extends": "animation/keyframe.json",
+  "extends": "animation/interpolating_keyframe.json",
   "properties": {
     "value": {
       "type": "Id",
diff --git a/dev/defs/animation/keyframe_string.json b/dev/defs/animation/keyframe_string.json
index 81fec8b..c4b301a 100644
--- a/dev/defs/animation/keyframe_string.json
+++ b/dev/defs/animation/keyframe_string.json
@@ -4,7 +4,7 @@
     "int": 142,
     "string": "keyframestring"
   },
-  "extends": "animation/keyframe.json",
+  "extends": "animation/interpolating_keyframe.json",
   "properties": {
     "value": {
       "type": "String",
diff --git a/dev/defs/animation/state_machine_fire_event.json b/dev/defs/animation/state_machine_fire_event.json
index 063ff2c..41db898 100644
--- a/dev/defs/animation/state_machine_fire_event.json
+++ b/dev/defs/animation/state_machine_fire_event.json
@@ -1,49 +1,49 @@
 {
-    "name": "StateMachineFireEvent",
-    "key": {
-        "int": 169,
-        "string": "statemachinefireevent"
+  "name": "StateMachineFireEvent",
+  "key": {
+    "int": 169,
+    "string": "statemachinefireevent"
+  },
+  "properties": {
+    "layerComponentId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 391,
+        "string": "layercomponentid"
+      },
+      "description": "Id of the transition or layer this belongs to.",
+      "runtime": false
     },
-    "properties": {
-        "layerComponentId": {
-            "type": "Id",
-            "initialValue": "Core.missingId",
-            "key": {
-                "int": 391,
-                "string": "layercomponentid"
-            },
-            "description": "Id of the transition or layer this belongs to.",
-            "runtime": false
-        },
-        "eventId": {
-            "type": "Id",
-            "typeRuntime": "uint",
-            "initialValue": "Core.missingId",
-            "initialValueRuntime": "-1",
-            "key": {
-                "int": 392,
-                "string": "eventid"
-            },
-            "description": "Id of the Event referenced."
-        },
-        "occursValue": {
-            "type": "uint",
-            "initialValue": "0",
-            "key": {
-                "int": 393,
-                "string": "occursvalue"
-            },
-            "description": "When the event fires."
-        },
-        "fireOrder": {
-            "type": "FractionalIndex",
-            "initialValue": "FractionalIndex.invalid",
-            "key": {
-                "int": 394,
-                "string": "fireorder"
-            },
-            "description": "Order value for sorting transitions in states.",
-            "runtime": false
-        }
+    "eventId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 392,
+        "string": "eventid"
+      },
+      "description": "Id of the Event referenced."
+    },
+    "occursValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 393,
+        "string": "occursvalue"
+      },
+      "description": "When the event fires."
+    },
+    "fireOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 394,
+        "string": "fireorder"
+      },
+      "description": "Order value for sorting transitions in states.",
+      "runtime": false
     }
+  }
 }
\ No newline at end of file
diff --git a/dev/defs/event.json b/dev/defs/event.json
index 900d71e..3cc5000 100644
--- a/dev/defs/event.json
+++ b/dev/defs/event.json
@@ -4,5 +4,15 @@
     "int": 128,
     "string": "event"
   },
-  "extends": "container_component.json"
+  "extends": "container_component.json",
+  "properties": {
+    "trigger": {
+      "type": "callback",
+      "animates": true,
+      "key": {
+        "int": 395,
+        "string": "trigger"
+      }
+    }
+  }
 }
\ No newline at end of file
diff --git a/include/rive/animation/animation_state_instance.hpp b/include/rive/animation/animation_state_instance.hpp
index 689b8ce..0eb15cc 100644
--- a/include/rive/animation/animation_state_instance.hpp
+++ b/include/rive/animation/animation_state_instance.hpp
@@ -19,7 +19,7 @@
 public:
     AnimationStateInstance(const AnimationState* animationState, ArtboardInstance* instance);
 
-    void advance(float seconds, Span<SMIInput*>) override;
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
     void apply(float mix) override;
 
     bool keepGoing() const override;
diff --git a/include/rive/animation/blend_state_1d_instance.hpp b/include/rive/animation/blend_state_1d_instance.hpp
index 5f97cde..d0c81a2 100644
--- a/include/rive/animation/blend_state_1d_instance.hpp
+++ b/include/rive/animation/blend_state_1d_instance.hpp
@@ -16,7 +16,7 @@
 
 public:
     BlendState1DInstance(const BlendState1D* blendState, ArtboardInstance* instance);
-    void advance(float seconds, Span<SMIInput*> inputs) override;
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
 };
 } // namespace rive
 #endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_direct_instance.hpp b/include/rive/animation/blend_state_direct_instance.hpp
index 8fd8cf8..75fcfdf 100644
--- a/include/rive/animation/blend_state_direct_instance.hpp
+++ b/include/rive/animation/blend_state_direct_instance.hpp
@@ -11,7 +11,7 @@
 {
 public:
     BlendStateDirectInstance(const BlendStateDirect* blendState, ArtboardInstance* instance);
-    void advance(float seconds, Span<SMIInput*> inputs) override;
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
 };
 } // namespace rive
 #endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_instance.hpp b/include/rive/animation/blend_state_instance.hpp
index 9f035cb..c95111a 100644
--- a/include/rive/animation/blend_state_instance.hpp
+++ b/include/rive/animation/blend_state_instance.hpp
@@ -6,6 +6,7 @@
 #include "rive/animation/state_instance.hpp"
 #include "rive/animation/blend_state.hpp"
 #include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
 
 namespace rive
 {
@@ -52,7 +53,7 @@
 
     bool keepGoing() const override { return m_KeepGoing; }
 
-    void advance(float seconds, Span<SMIInput*>) override
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override
     {
         // NOTE: we are intentionally ignoring the animationInstances' keepGoing
         // return value.
@@ -62,7 +63,7 @@
         {
             if (animation.m_AnimationInstance.keepGoing())
             {
-                animation.m_AnimationInstance.advance(seconds);
+                animation.m_AnimationInstance.advance(seconds, stateMachineInstance);
             }
         }
     }
diff --git a/include/rive/animation/interpolating_keyframe.hpp b/include/rive/animation/interpolating_keyframe.hpp
new file mode 100644
index 0000000..8255f77
--- /dev/null
+++ b/include/rive/animation/interpolating_keyframe.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_INTERPOLATING_KEY_FRAME_HPP_
+#define _RIVE_INTERPOLATING_KEY_FRAME_HPP_
+#include "rive/generated/animation/interpolating_keyframe_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class InterpolatingKeyFrame : public InterpolatingKeyFrameBase
+{
+public:
+    inline CubicInterpolator* interpolator() const { return m_interpolator; }
+    virtual void apply(Core* object, int propertyKey, float mix) = 0;
+    virtual void applyInterpolation(Core* object,
+                                    int propertyKey,
+                                    float seconds,
+                                    const KeyFrame* nextFrame,
+                                    float mix) = 0;
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+private:
+    CubicInterpolator* m_interpolator = nullptr;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyed_callback_reporter.hpp b/include/rive/animation/keyed_callback_reporter.hpp
new file mode 100644
index 0000000..7f691a3
--- /dev/null
+++ b/include/rive/animation/keyed_callback_reporter.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_KEYED_CALLBACK_REPORTER_HPP_
+#define _RIVE_KEYED_CALLBACK_REPORTER_HPP_
+
+namespace rive
+{
+class KeyedCallbackReporter
+{
+public:
+    virtual ~KeyedCallbackReporter() {}
+    virtual void reportKeyedCallback(uint32_t objectId,
+                                     uint32_t propertyKey,
+                                     float elapsedSeconds) = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyed_object.hpp b/include/rive/animation/keyed_object.hpp
index 70c97ab..04acdd6 100644
--- a/include/rive/animation/keyed_object.hpp
+++ b/include/rive/animation/keyed_object.hpp
@@ -6,11 +6,9 @@
 {
 class Artboard;
 class KeyedProperty;
+class KeyedCallbackReporter;
 class KeyedObject : public KeyedObjectBase
 {
-private:
-    std::vector<std::unique_ptr<KeyedProperty>> m_KeyedProperties;
-
 public:
     KeyedObject();
     ~KeyedObject() override;
@@ -18,9 +16,15 @@
 
     StatusCode onAddedDirty(CoreContext* context) override;
     StatusCode onAddedClean(CoreContext* context) override;
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              float secondsFrom,
+                              float secondsTo) const;
     void apply(Artboard* coreContext, float time, float mix);
 
     StatusCode import(ImportStack& importStack) override;
+
+private:
+    std::vector<std::unique_ptr<KeyedProperty>> m_keyedProperties;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/keyed_property.hpp b/include/rive/animation/keyed_property.hpp
index 29b8ffe..e092cfe 100644
--- a/include/rive/animation/keyed_property.hpp
+++ b/include/rive/animation/keyed_property.hpp
@@ -5,11 +5,9 @@
 namespace rive
 {
 class KeyFrame;
+class KeyedCallbackReporter;
 class KeyedProperty : public KeyedPropertyBase
 {
-private:
-    std::vector<std::unique_ptr<KeyFrame>> m_KeyFrames;
-
 public:
     KeyedProperty();
     ~KeyedProperty() override;
@@ -17,9 +15,20 @@
     StatusCode onAddedClean(CoreContext* context) override;
     StatusCode onAddedDirty(CoreContext* context) override;
 
+    /// Report any keyframes that occured between secondsFrom and secondsTo.
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              uint32_t objectId,
+                              float secondsFrom,
+                              float secondsTo) const;
+
+    /// Apply interpolating key frames.
     void apply(Core* object, float time, float mix);
 
     StatusCode import(ImportStack& importStack) override;
+
+private:
+    int closestFrameIndex(float seconds, int exactOffset = 0) const;
+    std::vector<std::unique_ptr<KeyFrame>> m_keyFrames;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/keyframe.hpp b/include/rive/animation/keyframe.hpp
index 961f2aa..a8b9397 100644
--- a/include/rive/animation/keyframe.hpp
+++ b/include/rive/animation/keyframe.hpp
@@ -7,25 +7,15 @@
 
 class KeyFrame : public KeyFrameBase
 {
-private:
-    CubicInterpolator* m_Interpolator = nullptr;
-    float m_Seconds;
-
 public:
-    inline float seconds() const { return m_Seconds; }
-    inline CubicInterpolator* interpolator() const { return m_Interpolator; }
+    inline float seconds() const { return m_seconds; }
 
     void computeSeconds(int fps);
 
-    StatusCode onAddedDirty(CoreContext* context) override;
-    virtual void apply(Core* object, int propertyKey, float mix) = 0;
-    virtual void applyInterpolation(Core* object,
-                                    int propertyKey,
-                                    float seconds,
-                                    const KeyFrame* nextFrame,
-                                    float mix) = 0;
-
     StatusCode import(ImportStack& importStack) override;
+
+private:
+    float m_seconds;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/keyframe_callback.hpp b/include/rive/animation/keyframe_callback.hpp
new file mode 100644
index 0000000..9c8c344
--- /dev/null
+++ b/include/rive/animation/keyframe_callback.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_KEY_FRAME_CALLBACK_HPP_
+#define _RIVE_KEY_FRAME_CALLBACK_HPP_
+#include "rive/generated/animation/keyframe_callback_base.hpp"
+
+namespace rive
+{
+class KeyFrameCallback : public KeyFrameCallbackBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/linear_animation.hpp b/include/rive/animation/linear_animation.hpp
index 6d8aeb5..2efcfeb 100644
--- a/include/rive/animation/linear_animation.hpp
+++ b/include/rive/animation/linear_animation.hpp
@@ -7,6 +7,7 @@
 {
 class Artboard;
 class KeyedObject;
+class KeyedCallbackReporter;
 
 class LinearAnimation : public LinearAnimationBase
 {
@@ -45,6 +46,10 @@
     // Used in testing to check how many animations gets deleted.
     static int deleteCount;
 #endif
+
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              float secondsFrom,
+                              float secondsTo) const;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp
index d7f6b25..ff5ea09 100644
--- a/include/rive/animation/linear_animation_instance.hpp
+++ b/include/rive/animation/linear_animation_instance.hpp
@@ -7,6 +7,7 @@
 namespace rive
 {
 class LinearAnimation;
+class KeyedCallbackReporter;
 
 class LinearAnimationInstance : public Scene
 {
@@ -17,7 +18,8 @@
 
     // Advance the animation by the specified time. Returns true if the
     // animation will continue to animate after this advance.
-    bool advance(float seconds);
+    bool advance(float seconds, KeyedCallbackReporter* reporter);
+    bool advance(float seconds) { return advance(seconds, nullptr); }
 
     void clearSpilledTime() { m_spilledTime = 0; }
 
diff --git a/include/rive/animation/state_instance.hpp b/include/rive/animation/state_instance.hpp
index 549555d..401b9f0 100644
--- a/include/rive/animation/state_instance.hpp
+++ b/include/rive/animation/state_instance.hpp
@@ -9,7 +9,7 @@
 namespace rive
 {
 class LayerState;
-class SMIInput;
+class StateMachineInstance;
 class ArtboardInstance;
 
 /// Represents an instance of a state tracked by the State Machine.
@@ -21,7 +21,7 @@
 public:
     StateInstance(const LayerState* layerState);
     virtual ~StateInstance();
-    virtual void advance(float seconds, Span<SMIInput*> inputs) = 0;
+    virtual void advance(float seconds, StateMachineInstance* stateMachineInstance) = 0;
     virtual void apply(float mix) = 0;
 
     /// Returns true when the State Machine needs to keep advancing this
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index e08e82d..cb1d7d8 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -5,6 +5,7 @@
 #include <stddef.h>
 #include <vector>
 #include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
 #include "rive/listener_type.hpp"
 #include "rive/scene.hpp"
 
@@ -22,10 +23,24 @@
 class HitShape;
 class NestedArtboard;
 class Event;
+class KeyedProperty;
 
-class StateMachineInstance : public Scene
+class EventReport
+{
+public:
+    EventReport(Event* event, float secondsDelay) : m_event(event), m_secondsDelay(secondsDelay) {}
+    Event* event() const { return m_event; }
+    float secondsDelay() const { return m_secondsDelay; }
+
+private:
+    Event* m_event;
+    float m_secondsDelay;
+};
+
+class StateMachineInstance : public Scene, public KeyedCallbackReporter
 {
     friend class SMIInput;
+    friend class KeyedProperty;
 
 private:
     void markNeedsAdvance();
@@ -84,17 +99,23 @@
     /// the backing artboard (explicitly not allowed on Scenes).
     Artboard* artboard() { return m_artboardInstance; }
 
-    /// Tracks an event that fired, will be cleared at the end of the next advance.
-    void fireEvent(Event* event);
+    /// Tracks an event that reported, will be cleared at the end of the next advance.
+    void reportEvent(Event* event, float secondsDelay = 0.0f);
 
-    /// Gets the number of events that fired since the last advance.
-    std::size_t firedEventCount() const;
+    /// Gets the number of events that reported since the last advance.
+    std::size_t reportedEventCount() const;
 
-    /// Gets a fired event at an index < firedEventCount().
-    const Event* firedEventAt(std::size_t index) const;
+    /// Gets a reported event at an index < reportedEventCount().
+    const EventReport reportedEventAt(std::size_t index) const;
+
+    /// Report which time based events have elapsed on a timeline within this
+    /// state machine.
+    void reportKeyedCallback(uint32_t objectId,
+                             uint32_t propertyKey,
+                             float elapsedSeconds) override;
 
 private:
-    std::vector<Event*> m_firedEvents;
+    std::vector<EventReport> m_reportedEvents;
     const StateMachine* m_machine;
     bool m_needsAdvance = false;
     std::vector<SMIInput*> m_inputInstances; // we own each pointer
diff --git a/include/rive/animation/state_transition.hpp b/include/rive/animation/state_transition.hpp
index 5fefb4c..ff37527 100644
--- a/include/rive/animation/state_transition.hpp
+++ b/include/rive/animation/state_transition.hpp
@@ -13,7 +13,7 @@
 class StateTransitionImporter;
 class TransitionCondition;
 class StateInstance;
-class SMIInput;
+class StateMachineInstance;
 class LinearAnimation;
 class LinearAnimationInstance;
 
@@ -59,7 +59,7 @@
     /// Returns AllowTransition::yes when this transition can be taken from
     /// stateFrom with the given inputs.
     AllowTransition allowed(StateInstance* stateFrom,
-                            Span<SMIInput*> inputs,
+                            StateMachineInstance* stateMachineInstance,
                             bool ignoreTriggers) const;
 
     /// Whether the animation is held at exit or if it keeps advancing
diff --git a/include/rive/animation/system_state_instance.hpp b/include/rive/animation/system_state_instance.hpp
index 0adc062..b439da8 100644
--- a/include/rive/animation/system_state_instance.hpp
+++ b/include/rive/animation/system_state_instance.hpp
@@ -6,6 +6,8 @@
 
 namespace rive
 {
+class StateMachineInstance;
+
 /// Represents an instance of a system state machine. Basically a
 /// placeholder that may have meaning to the state machine itself, or is
 /// just a no-op state (perhaps an unknown to this runtime state-type).
@@ -14,7 +16,7 @@
 public:
     SystemStateInstance(const LayerState* layerState, ArtboardInstance* instance);
 
-    void advance(float seconds, Span<SMIInput*> inputs) override;
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
     void apply(float mix) override;
 
     bool keepGoing() const override;
diff --git a/include/rive/core/field_types/core_callback_type.hpp b/include/rive/core/field_types/core_callback_type.hpp
new file mode 100644
index 0000000..047aaa0
--- /dev/null
+++ b/include/rive/core/field_types/core_callback_type.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_CORE_CALLBACK_TYPE_HPP_
+#define _RIVE_CORE_CALLBACK_TYPE_HPP_
+
+namespace rive
+{
+class StateMachineInstance;
+class CallbackData
+{
+public:
+    StateMachineInstance* context() const { return m_context; }
+    float delaySeconds() const { return m_delaySeconds; }
+    CallbackData(StateMachineInstance* context, float delaySeconds) :
+        m_context(context), m_delaySeconds(delaySeconds)
+    {}
+
+private:
+    StateMachineInstance* m_context;
+    float m_delaySeconds;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/event.hpp b/include/rive/event.hpp
index faa5518..743c6c5 100644
--- a/include/rive/event.hpp
+++ b/include/rive/event.hpp
@@ -1,12 +1,13 @@
 #ifndef _RIVE_EVENT_HPP_
 #define _RIVE_EVENT_HPP_
 #include "rive/generated/event_base.hpp"
-#include <stdio.h>
+
 namespace rive
 {
 class Event : public EventBase
 {
 public:
+    void trigger(const CallbackData& value) override;
 };
 } // namespace rive
 
diff --git a/include/rive/generated/animation/interpolating_keyframe_base.hpp b/include/rive/generated/animation/interpolating_keyframe_base.hpp
new file mode 100644
index 0000000..7e84216
--- /dev/null
+++ b/include/rive/generated/animation/interpolating_keyframe_base.hpp
@@ -0,0 +1,88 @@
+#ifndef _RIVE_INTERPOLATING_KEY_FRAME_BASE_HPP_
+#define _RIVE_INTERPOLATING_KEY_FRAME_BASE_HPP_
+#include "rive/animation/keyframe.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class InterpolatingKeyFrameBase : public KeyFrame
+{
+protected:
+    typedef KeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 170;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t interpolationTypePropertyKey = 68;
+    static const uint16_t interpolatorIdPropertyKey = 69;
+
+private:
+    uint32_t m_InterpolationType = 0;
+    uint32_t m_InterpolatorId = -1;
+
+public:
+    inline uint32_t interpolationType() const { return m_InterpolationType; }
+    void interpolationType(uint32_t value)
+    {
+        if (m_InterpolationType == value)
+        {
+            return;
+        }
+        m_InterpolationType = value;
+        interpolationTypeChanged();
+    }
+
+    inline uint32_t interpolatorId() const { return m_InterpolatorId; }
+    void interpolatorId(uint32_t value)
+    {
+        if (m_InterpolatorId == value)
+        {
+            return;
+        }
+        m_InterpolatorId = value;
+        interpolatorIdChanged();
+    }
+
+    void copy(const InterpolatingKeyFrameBase& object)
+    {
+        m_InterpolationType = object.m_InterpolationType;
+        m_InterpolatorId = object.m_InterpolatorId;
+        KeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case interpolationTypePropertyKey:
+                m_InterpolationType = CoreUintType::deserialize(reader);
+                return true;
+            case interpolatorIdPropertyKey:
+                m_InterpolatorId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return KeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void interpolationTypeChanged() {}
+    virtual void interpolatorIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_base.hpp b/include/rive/generated/animation/keyframe_base.hpp
index 7570d87..9e8fa47 100644
--- a/include/rive/generated/animation/keyframe_base.hpp
+++ b/include/rive/generated/animation/keyframe_base.hpp
@@ -28,13 +28,9 @@
     uint16_t coreType() const override { return typeKey; }
 
     static const uint16_t framePropertyKey = 67;
-    static const uint16_t interpolationTypePropertyKey = 68;
-    static const uint16_t interpolatorIdPropertyKey = 69;
 
 private:
     uint32_t m_Frame = 0;
-    uint32_t m_InterpolationType = 0;
-    uint32_t m_InterpolatorId = -1;
 
 public:
     inline uint32_t frame() const { return m_Frame; }
@@ -48,34 +44,7 @@
         frameChanged();
     }
 
-    inline uint32_t interpolationType() const { return m_InterpolationType; }
-    void interpolationType(uint32_t value)
-    {
-        if (m_InterpolationType == value)
-        {
-            return;
-        }
-        m_InterpolationType = value;
-        interpolationTypeChanged();
-    }
-
-    inline uint32_t interpolatorId() const { return m_InterpolatorId; }
-    void interpolatorId(uint32_t value)
-    {
-        if (m_InterpolatorId == value)
-        {
-            return;
-        }
-        m_InterpolatorId = value;
-        interpolatorIdChanged();
-    }
-
-    void copy(const KeyFrameBase& object)
-    {
-        m_Frame = object.m_Frame;
-        m_InterpolationType = object.m_InterpolationType;
-        m_InterpolatorId = object.m_InterpolatorId;
-    }
+    void copy(const KeyFrameBase& object) { m_Frame = object.m_Frame; }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
     {
@@ -84,20 +53,12 @@
             case framePropertyKey:
                 m_Frame = CoreUintType::deserialize(reader);
                 return true;
-            case interpolationTypePropertyKey:
-                m_InterpolationType = CoreUintType::deserialize(reader);
-                return true;
-            case interpolatorIdPropertyKey:
-                m_InterpolatorId = CoreUintType::deserialize(reader);
-                return true;
         }
         return false;
     }
 
 protected:
     virtual void frameChanged() {}
-    virtual void interpolationTypeChanged() {}
-    virtual void interpolatorIdChanged() {}
 };
 } // namespace rive
 
diff --git a/include/rive/generated/animation/keyframe_bool_base.hpp b/include/rive/generated/animation/keyframe_bool_base.hpp
index c20a703..ffe12be 100644
--- a/include/rive/generated/animation/keyframe_bool_base.hpp
+++ b/include/rive/generated/animation/keyframe_bool_base.hpp
@@ -1,13 +1,13 @@
 #ifndef _RIVE_KEY_FRAME_BOOL_BASE_HPP_
 #define _RIVE_KEY_FRAME_BOOL_BASE_HPP_
-#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/core/field_types/core_bool_type.hpp"
 namespace rive
 {
-class KeyFrameBoolBase : public KeyFrame
+class KeyFrameBoolBase : public InterpolatingKeyFrame
 {
 protected:
-    typedef KeyFrame Super;
+    typedef InterpolatingKeyFrame Super;
 
 public:
     static const uint16_t typeKey = 84;
@@ -19,6 +19,7 @@
         switch (typeKey)
         {
             case KeyFrameBoolBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
             case KeyFrameBase::typeKey:
                 return true;
             default:
@@ -49,7 +50,7 @@
     void copy(const KeyFrameBoolBase& object)
     {
         m_Value = object.m_Value;
-        KeyFrame::copy(object);
+        InterpolatingKeyFrame::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -60,7 +61,7 @@
                 m_Value = CoreBoolType::deserialize(reader);
                 return true;
         }
-        return KeyFrame::deserialize(propertyKey, reader);
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/animation/keyframe_callback_base.hpp b/include/rive/generated/animation/keyframe_callback_base.hpp
new file mode 100644
index 0000000..066256e
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_callback_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_KEY_FRAME_CALLBACK_BASE_HPP_
+#define _RIVE_KEY_FRAME_CALLBACK_BASE_HPP_
+#include "rive/animation/keyframe.hpp"
+namespace rive
+{
+class KeyFrameCallbackBase : public KeyFrame
+{
+protected:
+    typedef KeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 171;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameCallbackBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_color_base.hpp b/include/rive/generated/animation/keyframe_color_base.hpp
index 16d80fe..4814a47 100644
--- a/include/rive/generated/animation/keyframe_color_base.hpp
+++ b/include/rive/generated/animation/keyframe_color_base.hpp
@@ -1,13 +1,13 @@
 #ifndef _RIVE_KEY_FRAME_COLOR_BASE_HPP_
 #define _RIVE_KEY_FRAME_COLOR_BASE_HPP_
-#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/core/field_types/core_color_type.hpp"
 namespace rive
 {
-class KeyFrameColorBase : public KeyFrame
+class KeyFrameColorBase : public InterpolatingKeyFrame
 {
 protected:
-    typedef KeyFrame Super;
+    typedef InterpolatingKeyFrame Super;
 
 public:
     static const uint16_t typeKey = 37;
@@ -19,6 +19,7 @@
         switch (typeKey)
         {
             case KeyFrameColorBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
             case KeyFrameBase::typeKey:
                 return true;
             default:
@@ -49,7 +50,7 @@
     void copy(const KeyFrameColorBase& object)
     {
         m_Value = object.m_Value;
-        KeyFrame::copy(object);
+        InterpolatingKeyFrame::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -60,7 +61,7 @@
                 m_Value = CoreColorType::deserialize(reader);
                 return true;
         }
-        return KeyFrame::deserialize(propertyKey, reader);
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/animation/keyframe_double_base.hpp b/include/rive/generated/animation/keyframe_double_base.hpp
index df52746..0ee26f0 100644
--- a/include/rive/generated/animation/keyframe_double_base.hpp
+++ b/include/rive/generated/animation/keyframe_double_base.hpp
@@ -1,13 +1,13 @@
 #ifndef _RIVE_KEY_FRAME_DOUBLE_BASE_HPP_
 #define _RIVE_KEY_FRAME_DOUBLE_BASE_HPP_
-#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/core/field_types/core_double_type.hpp"
 namespace rive
 {
-class KeyFrameDoubleBase : public KeyFrame
+class KeyFrameDoubleBase : public InterpolatingKeyFrame
 {
 protected:
-    typedef KeyFrame Super;
+    typedef InterpolatingKeyFrame Super;
 
 public:
     static const uint16_t typeKey = 30;
@@ -19,6 +19,7 @@
         switch (typeKey)
         {
             case KeyFrameDoubleBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
             case KeyFrameBase::typeKey:
                 return true;
             default:
@@ -49,7 +50,7 @@
     void copy(const KeyFrameDoubleBase& object)
     {
         m_Value = object.m_Value;
-        KeyFrame::copy(object);
+        InterpolatingKeyFrame::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -60,7 +61,7 @@
                 m_Value = CoreDoubleType::deserialize(reader);
                 return true;
         }
-        return KeyFrame::deserialize(propertyKey, reader);
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/animation/keyframe_id_base.hpp b/include/rive/generated/animation/keyframe_id_base.hpp
index 086f5c9..229b1a3 100644
--- a/include/rive/generated/animation/keyframe_id_base.hpp
+++ b/include/rive/generated/animation/keyframe_id_base.hpp
@@ -1,13 +1,13 @@
 #ifndef _RIVE_KEY_FRAME_ID_BASE_HPP_
 #define _RIVE_KEY_FRAME_ID_BASE_HPP_
-#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/core/field_types/core_uint_type.hpp"
 namespace rive
 {
-class KeyFrameIdBase : public KeyFrame
+class KeyFrameIdBase : public InterpolatingKeyFrame
 {
 protected:
-    typedef KeyFrame Super;
+    typedef InterpolatingKeyFrame Super;
 
 public:
     static const uint16_t typeKey = 50;
@@ -19,6 +19,7 @@
         switch (typeKey)
         {
             case KeyFrameIdBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
             case KeyFrameBase::typeKey:
                 return true;
             default:
@@ -49,7 +50,7 @@
     void copy(const KeyFrameIdBase& object)
     {
         m_Value = object.m_Value;
-        KeyFrame::copy(object);
+        InterpolatingKeyFrame::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -60,7 +61,7 @@
                 m_Value = CoreUintType::deserialize(reader);
                 return true;
         }
-        return KeyFrame::deserialize(propertyKey, reader);
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/animation/keyframe_string_base.hpp b/include/rive/generated/animation/keyframe_string_base.hpp
index cadab51..e1a54c7 100644
--- a/include/rive/generated/animation/keyframe_string_base.hpp
+++ b/include/rive/generated/animation/keyframe_string_base.hpp
@@ -1,14 +1,14 @@
 #ifndef _RIVE_KEY_FRAME_STRING_BASE_HPP_
 #define _RIVE_KEY_FRAME_STRING_BASE_HPP_
 #include <string>
-#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/core/field_types/core_string_type.hpp"
 namespace rive
 {
-class KeyFrameStringBase : public KeyFrame
+class KeyFrameStringBase : public InterpolatingKeyFrame
 {
 protected:
-    typedef KeyFrame Super;
+    typedef InterpolatingKeyFrame Super;
 
 public:
     static const uint16_t typeKey = 142;
@@ -20,6 +20,7 @@
         switch (typeKey)
         {
             case KeyFrameStringBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
             case KeyFrameBase::typeKey:
                 return true;
             default:
@@ -50,7 +51,7 @@
     void copy(const KeyFrameStringBase& object)
     {
         m_Value = object.m_Value;
-        KeyFrame::copy(object);
+        InterpolatingKeyFrame::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -61,7 +62,7 @@
                 m_Value = CoreStringType::deserialize(reader);
                 return true;
         }
-        return KeyFrame::deserialize(propertyKey, reader);
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 7984af9..f062ab5 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -17,10 +17,12 @@
 #include "rive/animation/cubic_value_interpolator.hpp"
 #include "rive/animation/entry_state.hpp"
 #include "rive/animation/exit_state.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
 #include "rive/animation/keyed_object.hpp"
 #include "rive/animation/keyed_property.hpp"
 #include "rive/animation/keyframe.hpp"
 #include "rive/animation/keyframe_bool.hpp"
+#include "rive/animation/keyframe_callback.hpp"
 #include "rive/animation/keyframe_color.hpp"
 #include "rive/animation/keyframe_double.hpp"
 #include "rive/animation/keyframe_id.hpp"
@@ -252,6 +254,8 @@
                 return new NestedNumber();
             case BlendState1DBase::typeKey:
                 return new BlendState1D();
+            case KeyFrameCallbackBase::typeKey:
+                return new KeyFrameCallback();
             case NestedRemapAnimationBase::typeKey:
                 return new NestedRemapAnimation();
             case TransitionBoolConditionBase::typeKey:
@@ -478,11 +482,11 @@
             case KeyFrameBase::framePropertyKey:
                 object->as<KeyFrameBase>()->frame(value);
                 break;
-            case KeyFrameBase::interpolationTypePropertyKey:
-                object->as<KeyFrameBase>()->interpolationType(value);
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+                object->as<InterpolatingKeyFrameBase>()->interpolationType(value);
                 break;
-            case KeyFrameBase::interpolatorIdPropertyKey:
-                object->as<KeyFrameBase>()->interpolatorId(value);
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+                object->as<InterpolatingKeyFrameBase>()->interpolatorId(value);
                 break;
             case KeyFrameIdBase::valuePropertyKey:
                 object->as<KeyFrameIdBase>()->value(value);
@@ -1146,6 +1150,15 @@
                 break;
         }
     }
+    static void setCallback(Core* object, int propertyKey, CallbackData value)
+    {
+        switch (propertyKey)
+        {
+            case EventBase::triggerPropertyKey:
+                object->as<EventBase>()->trigger(value);
+                break;
+        }
+    }
     static std::string getString(Core* object, int propertyKey)
     {
         switch (propertyKey)
@@ -1227,10 +1240,10 @@
                 return object->as<StateMachineListenerBase>()->listenerTypeValue();
             case KeyFrameBase::framePropertyKey:
                 return object->as<KeyFrameBase>()->frame();
-            case KeyFrameBase::interpolationTypePropertyKey:
-                return object->as<KeyFrameBase>()->interpolationType();
-            case KeyFrameBase::interpolatorIdPropertyKey:
-                return object->as<KeyFrameBase>()->interpolatorId();
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+                return object->as<InterpolatingKeyFrameBase>()->interpolationType();
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+                return object->as<InterpolatingKeyFrameBase>()->interpolatorId();
             case KeyFrameIdBase::valuePropertyKey:
                 return object->as<KeyFrameIdBase>()->value();
             case ListenerBoolChangeBase::valuePropertyKey:
@@ -1723,8 +1736,8 @@
             case StateMachineListenerBase::targetIdPropertyKey:
             case StateMachineListenerBase::listenerTypeValuePropertyKey:
             case KeyFrameBase::framePropertyKey:
-            case KeyFrameBase::interpolationTypePropertyKey:
-            case KeyFrameBase::interpolatorIdPropertyKey:
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
             case KeyFrameIdBase::valuePropertyKey:
             case ListenerBoolChangeBase::valuePropertyKey:
             case ListenerAlignTargetBase::targetIdPropertyKey:
@@ -1950,6 +1963,16 @@
                 return -1;
         }
     }
+    static bool isCallback(uint32_t propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case EventBase::triggerPropertyKey:
+                return true;
+            default:
+                return false;
+        }
+    }
 };
 } // namespace rive
 
diff --git a/include/rive/generated/event_base.hpp b/include/rive/generated/event_base.hpp
index b2d4017..dd04e08 100644
--- a/include/rive/generated/event_base.hpp
+++ b/include/rive/generated/event_base.hpp
@@ -1,6 +1,7 @@
 #ifndef _RIVE_EVENT_BASE_HPP_
 #define _RIVE_EVENT_BASE_HPP_
 #include "rive/container_component.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
 namespace rive
 {
 class EventBase : public ContainerComponent
@@ -28,6 +29,11 @@
 
     uint16_t coreType() const override { return typeKey; }
 
+    static const uint16_t triggerPropertyKey = 395;
+
+public:
+    virtual void trigger(const CallbackData& value) = 0;
+
     Core* clone() const override;
 
 protected:
diff --git a/src/animation/animation_state_instance.cpp b/src/animation/animation_state_instance.cpp
index a2ed1ed..0739550 100644
--- a/src/animation/animation_state_instance.cpp
+++ b/src/animation/animation_state_instance.cpp
@@ -1,5 +1,6 @@
 #include "rive/animation/animation_state_instance.hpp"
 #include "rive/animation/animation_state.hpp"
+#include "rive/animation/state_machine_instance.hpp"
 
 using namespace rive;
 
@@ -23,9 +24,10 @@
 
 // NOTE:: should we return bool here? we are not currently using the output of this, we are instead
 // using m_keepGoing directly.
-void AnimationStateInstance::advance(float seconds, Span<SMIInput*>)
+void AnimationStateInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
 {
-    m_KeepGoing = m_AnimationInstance.advance(seconds * state()->as<AnimationState>()->speed());
+    m_KeepGoing = m_AnimationInstance.advance(seconds * state()->as<AnimationState>()->speed(),
+                                              stateMachineInstance);
 }
 
 void AnimationStateInstance::apply(float mix) { m_AnimationInstance.apply(mix); }
diff --git a/src/animation/blend_state_1d_instance.cpp b/src/animation/blend_state_1d_instance.cpp
index 29e76da..6902273 100644
--- a/src/animation/blend_state_1d_instance.cpp
+++ b/src/animation/blend_state_1d_instance.cpp
@@ -39,16 +39,16 @@
     return idx;
 }
 
-void BlendState1DInstance::advance(float seconds, Span<SMIInput*> inputs)
+void BlendState1DInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
 {
-    BlendStateInstance<BlendState1D, BlendAnimation1D>::advance(seconds, inputs);
+    BlendStateInstance<BlendState1D, BlendAnimation1D>::advance(seconds, stateMachineInstance);
 
     auto blendState = state()->as<BlendState1D>();
     float value = 0.0f;
     if (blendState->hasValidInputId())
     {
         // TODO: https://github.com/rive-app/rive-cpp/issues/229
-        auto inputInstance = inputs[blendState->inputId()];
+        auto inputInstance = stateMachineInstance->input(blendState->inputId());
         auto numberInput = static_cast<const SMINumber*>(inputInstance);
         value = numberInput->value();
     }
diff --git a/src/animation/blend_state_direct_instance.cpp b/src/animation/blend_state_direct_instance.cpp
index be42eaf..5c52763 100644
--- a/src/animation/blend_state_direct_instance.cpp
+++ b/src/animation/blend_state_direct_instance.cpp
@@ -11,9 +11,11 @@
     BlendStateInstance<BlendStateDirect, BlendAnimationDirect>(blendState, instance)
 {}
 
-void BlendStateDirectInstance::advance(float seconds, Span<SMIInput*> inputs)
+void BlendStateDirectInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
 {
-    BlendStateInstance<BlendStateDirect, BlendAnimationDirect>::advance(seconds, inputs);
+    BlendStateInstance<BlendStateDirect, BlendAnimationDirect>::advance(seconds,
+                                                                        stateMachineInstance);
+
     for (auto& animation : m_AnimationInstances)
     {
         if (animation.blendAnimation()->blendSource() ==
@@ -24,7 +26,7 @@
         }
         else
         {
-            auto inputInstance = inputs[animation.blendAnimation()->inputId()];
+            auto inputInstance = stateMachineInstance->input(animation.blendAnimation()->inputId());
             auto numberInput = static_cast<const SMINumber*>(inputInstance);
             auto value = numberInput->value();
             animation.mix(std::min(1.0f, std::max(0.0f, value / 100.0f)));
diff --git a/src/animation/interpolating_keyframe.cpp b/src/animation/interpolating_keyframe.cpp
new file mode 100644
index 0000000..5330538
--- /dev/null
+++ b/src/animation/interpolating_keyframe.cpp
@@ -0,0 +1,20 @@
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+StatusCode InterpolatingKeyFrame::onAddedDirty(CoreContext* context)
+{
+    if (interpolatorId() != -1)
+    {
+        auto coreObject = context->resolve(interpolatorId());
+        if (coreObject == nullptr || !coreObject->is<CubicInterpolator>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_interpolator = coreObject->as<CubicInterpolator>();
+    }
+
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/animation/keyed_object.cpp b/src/animation/keyed_object.cpp
index 6866579..c43e9de 100644
--- a/src/animation/keyed_object.cpp
+++ b/src/animation/keyed_object.cpp
@@ -3,6 +3,7 @@
 #include "rive/animation/linear_animation.hpp"
 #include "rive/artboard.hpp"
 #include "rive/importers/linear_animation_importer.hpp"
+#include "rive/generated/core_registry.hpp"
 
 using namespace rive;
 
@@ -11,7 +12,7 @@
 
 void KeyedObject::addKeyedProperty(std::unique_ptr<KeyedProperty> property)
 {
-    m_KeyedProperties.push_back(std::move(property));
+    m_keyedProperties.push_back(std::move(property));
 }
 
 StatusCode KeyedObject::onAddedDirty(CoreContext* context)
@@ -22,7 +23,7 @@
         return StatusCode::MissingObject;
     }
 
-    for (auto& property : m_KeyedProperties)
+    for (auto& property : m_keyedProperties)
     {
         StatusCode code;
         if ((code = property->onAddedDirty(context)) != StatusCode::Ok)
@@ -35,13 +36,27 @@
 
 StatusCode KeyedObject::onAddedClean(CoreContext* context)
 {
-    for (auto& property : m_KeyedProperties)
+    for (auto& property : m_keyedProperties)
     {
         property->onAddedClean(context);
     }
     return StatusCode::Ok;
 }
 
+void KeyedObject::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                       float secondsFrom,
+                                       float secondsTo) const
+{
+    for (const std::unique_ptr<KeyedProperty>& property : m_keyedProperties)
+    {
+        if (!CoreRegistry::isCallback(property->propertyKey()))
+        {
+            continue;
+        }
+        property->reportKeyedCallbacks(reporter, objectId(), secondsFrom, secondsTo);
+    }
+}
+
 void KeyedObject::apply(Artboard* artboard, float time, float mix)
 {
     Core* object = artboard->resolve(objectId());
@@ -49,8 +64,12 @@
     {
         return;
     }
-    for (auto& property : m_KeyedProperties)
+    for (std::unique_ptr<KeyedProperty>& property : m_keyedProperties)
     {
+        if (CoreRegistry::isCallback(property->propertyKey()))
+        {
+            continue;
+        }
         property->apply(object, time, mix);
     }
 }
diff --git a/src/animation/keyed_property.cpp b/src/animation/keyed_property.cpp
index 8aa58d5..b200df7 100644
--- a/src/animation/keyed_property.cpp
+++ b/src/animation/keyed_property.cpp
@@ -1,6 +1,8 @@
 #include "rive/animation/keyed_property.hpp"
 #include "rive/animation/keyed_object.hpp"
 #include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
 #include "rive/importers/import_stack.hpp"
 #include "rive/importers/keyed_object_importer.hpp"
 
@@ -11,23 +13,22 @@
 
 void KeyedProperty::addKeyFrame(std::unique_ptr<KeyFrame> keyframe)
 {
-    m_KeyFrames.push_back(std::move(keyframe));
+    m_keyFrames.push_back(std::move(keyframe));
 }
 
-void KeyedProperty::apply(Core* object, float seconds, float mix)
+int KeyedProperty::closestFrameIndex(float seconds, int exactOffset) const
 {
-    assert(!m_KeyFrames.empty());
-
     int idx = 0;
     int mid = 0;
-    float closestSeconds = 0.0f;
+    float closestSeconds = 0;
     int start = 0;
-    auto numKeyFrames = static_cast<int>(m_KeyFrames.size());
+    auto numKeyFrames = static_cast<int>(m_keyFrames.size());
     int end = numKeyFrames - 1;
+
     while (start <= end)
     {
         mid = (start + end) >> 1;
-        closestSeconds = m_KeyFrames[mid]->seconds();
+        closestSeconds = m_keyFrames[mid]->seconds();
         if (closestSeconds < seconds)
         {
             start = mid + 1;
@@ -38,23 +39,54 @@
         }
         else
         {
-            idx = start = mid;
-            break;
+            return mid + exactOffset;
         }
         idx = start;
     }
+    return idx;
+}
+
+void KeyedProperty::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                         uint32_t objectId,
+                                         float secondsFrom,
+                                         float secondsTo) const
+{
+    int idx = closestFrameIndex(secondsFrom, 1);
+    int idxTo = closestFrameIndex(secondsTo, 1);
+
+    if (idxTo < idx)
+    {
+        auto swap = idx;
+        idx = idxTo;
+        idxTo = swap;
+    }
+    while (idxTo > idx)
+    {
+        const std::unique_ptr<KeyFrame>& frame = m_keyFrames[idx];
+        reporter->reportKeyedCallback(objectId, propertyKey(), secondsTo - frame->seconds());
+        idx++;
+    }
+}
+
+void KeyedProperty::apply(Core* object, float seconds, float mix)
+{
+    assert(!m_keyFrames.empty());
+
+    int idx = closestFrameIndex(seconds);
     int pk = propertyKey();
 
     if (idx == 0)
     {
-        m_KeyFrames[0]->apply(object, pk, mix);
+        static_cast<InterpolatingKeyFrame*>(m_keyFrames[0].get())->apply(object, pk, mix);
     }
     else
     {
-        if (idx < numKeyFrames)
+        if (idx < static_cast<int>(m_keyFrames.size()))
         {
-            KeyFrame* fromFrame = m_KeyFrames[idx - 1].get();
-            KeyFrame* toFrame = m_KeyFrames[idx].get();
+            InterpolatingKeyFrame* fromFrame =
+                static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx - 1].get());
+            InterpolatingKeyFrame* toFrame =
+                static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx].get());
             if (seconds == toFrame->seconds())
             {
                 toFrame->apply(object, pk, mix);
@@ -73,7 +105,7 @@
         }
         else
         {
-            m_KeyFrames[idx - 1]->apply(object, pk, mix);
+            static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx - 1].get())->apply(object, pk, mix);
         }
     }
 }
@@ -81,7 +113,7 @@
 StatusCode KeyedProperty::onAddedDirty(CoreContext* context)
 {
     StatusCode code;
-    for (auto& keyframe : m_KeyFrames)
+    for (auto& keyframe : m_keyFrames)
     {
         if ((code = keyframe->onAddedDirty(context)) != StatusCode::Ok)
         {
@@ -94,7 +126,7 @@
 StatusCode KeyedProperty::onAddedClean(CoreContext* context)
 {
     StatusCode code;
-    for (auto& keyframe : m_KeyFrames)
+    for (auto& keyframe : m_keyFrames)
     {
         if ((code = keyframe->onAddedClean(context)) != StatusCode::Ok)
         {
diff --git a/src/animation/keyframe.cpp b/src/animation/keyframe.cpp
index 671beb8..9989fd7 100644
--- a/src/animation/keyframe.cpp
+++ b/src/animation/keyframe.cpp
@@ -7,22 +7,7 @@
 
 using namespace rive;
 
-StatusCode KeyFrame::onAddedDirty(CoreContext* context)
-{
-    if (interpolatorId() != -1)
-    {
-        auto coreObject = context->resolve(interpolatorId());
-        if (coreObject == nullptr || !coreObject->is<CubicInterpolator>())
-        {
-            return StatusCode::MissingObject;
-        }
-        m_Interpolator = coreObject->as<CubicInterpolator>();
-    }
-
-    return StatusCode::Ok;
-}
-
-void KeyFrame::computeSeconds(int fps) { m_Seconds = frame() / (float)fps; }
+void KeyFrame::computeSeconds(int fps) { m_seconds = frame() / (float)fps; }
 
 StatusCode KeyFrame::import(ImportStack& importStack)
 {
diff --git a/src/animation/keyframe_callback.cpp b/src/animation/keyframe_callback.cpp
new file mode 100644
index 0000000..4989039
--- /dev/null
+++ b/src/animation/keyframe_callback.cpp
@@ -0,0 +1,2 @@
+#include "rive/animation/keyframe_callback.hpp"
+#include "rive/core_context.hpp"
diff --git a/src/animation/linear_animation.cpp b/src/animation/linear_animation.cpp
index 1db8f58..bb38502 100644
--- a/src/animation/linear_animation.cpp
+++ b/src/animation/linear_animation.cpp
@@ -1,5 +1,6 @@
 #include "rive/animation/linear_animation.hpp"
 #include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
 #include "rive/artboard.hpp"
 #include "rive/importers/artboard_importer.hpp"
 #include "rive/importers/import_stack.hpp"
@@ -114,4 +115,14 @@
             return direction == 0 ? localTime + startTime() : endTime() - localTime;
     }
     RIVE_UNREACHABLE();
+}
+
+void LinearAnimation::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                           float secondsFrom,
+                                           float secondsTo) const
+{
+    for (const auto& object : m_KeyedObjects)
+    {
+        object->reportKeyedCallbacks(reporter, secondsFrom, secondsTo);
+    }
 }
\ No newline at end of file
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp
index 7be5d6a..76049f5 100644
--- a/src/animation/linear_animation_instance.cpp
+++ b/src/animation/linear_animation_instance.cpp
@@ -1,6 +1,7 @@
 #include "rive/animation/linear_animation_instance.hpp"
 #include "rive/animation/linear_animation.hpp"
 #include "rive/animation/loop.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
 #include "rive/rive_counter.hpp"
 #include <cmath>
 #include <cassert>
@@ -48,7 +49,7 @@
     return more;
 }
 
-bool LinearAnimationInstance::advance(float elapsedSeconds)
+bool LinearAnimationInstance::advance(float elapsedSeconds, KeyedCallbackReporter* reporter)
 {
     const LinearAnimation& animation = *m_animation;
     float deltaSeconds = elapsedSeconds * animation.speed() * m_direction;
@@ -67,7 +68,12 @@
     // stop gap before we move spilled tracking into state machine logic.
     bool killSpilledTime = !this->keepGoing();
 
+    float lastTime = m_time;
     m_time += deltaSeconds;
+    if (reporter != nullptr)
+    {
+        animation.reportKeyedCallbacks(reporter, lastTime, m_time);
+    }
 
     int fps = animation.fps();
     float frames = m_time * fps;
diff --git a/src/animation/listener_fire_event.cpp b/src/animation/listener_fire_event.cpp
index 39d2fd1..41e8dc6 100644
--- a/src/animation/listener_fire_event.cpp
+++ b/src/animation/listener_fire_event.cpp
@@ -11,5 +11,5 @@
     {
         return;
     }
-    stateMachineInstance->fireEvent(coreEvent->as<Event>());
+    stateMachineInstance->reportEvent(coreEvent->as<Event>());
 }
\ No newline at end of file
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index cc6c649..a01685d 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -23,6 +23,8 @@
 #include "rive/nested_artboard.hpp"
 #include "rive/rive_counter.hpp"
 #include "rive/shapes/shape.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/generated/core_registry.hpp"
 #include <unordered_map>
 
 using namespace rive;
@@ -69,20 +71,20 @@
         }
     }
 
-    bool advance(float seconds, Span<SMIInput*> inputs)
+    bool advance(float seconds)
     {
         m_stateMachineChangedOnAdvance = false;
-        m_currentState->advance(seconds, inputs);
+        m_currentState->advance(seconds, m_stateMachineInstance);
         updateMix(seconds);
 
         if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom)
         {
             // This didn't advance during our updateState, but it should now
             // that we realize we need to mix it in.
-            m_stateFrom->advance(seconds, inputs);
+            m_stateFrom->advance(seconds, m_stateMachineInstance);
         }
 
-        for (int i = 0; updateState(inputs, i != 0); i++)
+        for (int i = 0; updateState(i != 0); i++)
         {
             apply();
 
@@ -107,7 +109,7 @@
                m_mix < 1.0f;
     }
 
-    bool updateState(Span<SMIInput*> inputs, bool ignoreTriggers)
+    bool updateState(bool ignoreTriggers)
     {
         // Don't allow changing state while a transition is taking place
         // (we're mixing one state onto another).
@@ -118,12 +120,12 @@
 
         m_waitingForExit = false;
 
-        if (tryChangeState(m_anyStateInstance, inputs, ignoreTriggers))
+        if (tryChangeState(m_anyStateInstance, ignoreTriggers))
         {
             return true;
         }
 
-        return tryChangeState(m_currentState, inputs, ignoreTriggers);
+        return tryChangeState(m_currentState, ignoreTriggers);
     }
 
     void fireEvents(StateMachineFireOccurance occurs,
@@ -162,9 +164,7 @@
         return true;
     }
 
-    bool tryChangeState(StateInstance* stateFromInstance,
-                        Span<SMIInput*> inputs,
-                        bool ignoreTriggers)
+    bool tryChangeState(StateInstance* stateFromInstance, bool ignoreTriggers)
     {
         if (stateFromInstance == nullptr)
         {
@@ -175,7 +175,8 @@
         for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
         {
             auto transition = stateFrom->transition(i);
-            auto allowed = transition->allowed(stateFromInstance, inputs, ignoreTriggers);
+            auto allowed =
+                transition->allowed(stateFromInstance, m_stateMachineInstance, ignoreTriggers);
             if (allowed == AllowTransition::yes && changeState(transition->stateTo()))
             {
                 m_stateMachineChangedOnAdvance = true;
@@ -226,7 +227,7 @@
                         static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
 
                     auto spilledTime = instance->spilledTime();
-                    m_currentState->advance(spilledTime, inputs);
+                    m_currentState->advance(spilledTime, m_stateMachineInstance);
                 }
                 m_mix = 0.0f;
                 updateMix(0.0f);
@@ -524,11 +525,11 @@
 
 bool StateMachineInstance::advance(float seconds)
 {
-    m_firedEvents.clear();
+    m_reportedEvents.clear();
     m_needsAdvance = false;
     for (size_t i = 0; i < m_layerCount; i++)
     {
-        if (m_layers[i].advance(seconds, m_inputInstances))
+        if (m_layers[i].advance(seconds))
         {
             m_needsAdvance = true;
         }
@@ -650,15 +651,27 @@
     return nullptr;
 }
 
-void StateMachineInstance::fireEvent(Event* event) { m_firedEvents.push_back(event); }
-
-std::size_t StateMachineInstance::firedEventCount() const { return m_firedEvents.size(); }
-
-const Event* StateMachineInstance::firedEventAt(std::size_t index) const
+void StateMachineInstance::reportEvent(Event* event, float delaySeconds)
 {
-    if (index >= m_firedEvents.size())
-    {
-        return nullptr;
-    }
-    return m_firedEvents[index];
+    m_reportedEvents.push_back(EventReport(event, delaySeconds));
 }
+
+std::size_t StateMachineInstance::reportedEventCount() const { return m_reportedEvents.size(); }
+
+const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const
+{
+    if (index >= m_reportedEvents.size())
+    {
+        return EventReport(nullptr, 0.0f);
+    }
+    return m_reportedEvents[index];
+}
+
+void StateMachineInstance::reportKeyedCallback(uint32_t objectId,
+                                               uint32_t propertyKey,
+                                               float elapsedSeconds)
+{
+    auto coreObject = m_artboardInstance->resolve(objectId);
+    CallbackData data(this, elapsedSeconds);
+    CoreRegistry::setCallback(coreObject, propertyKey, data);
+}
\ No newline at end of file
diff --git a/src/animation/state_transition.cpp b/src/animation/state_transition.cpp
index 208790b..1d9bf08 100644
--- a/src/animation/state_transition.cpp
+++ b/src/animation/state_transition.cpp
@@ -8,6 +8,7 @@
 #include "rive/animation/state_transition.hpp"
 #include "rive/animation/transition_condition.hpp"
 #include "rive/animation/transition_trigger_condition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
 #include "rive/importers/import_stack.hpp"
 #include "rive/importers/layer_state_importer.hpp"
 
@@ -136,7 +137,7 @@
 }
 
 AllowTransition StateTransition::allowed(StateInstance* stateFrom,
-                                         Span<SMIInput*> inputs,
+                                         StateMachineInstance* stateMachineInstance,
                                          bool ignoreTriggers) const
 {
     if (isDisabled())
@@ -147,7 +148,7 @@
     for (auto condition : m_Conditions)
     {
         // N.B. state machine instance sanitizes these for us...
-        auto input = inputs[condition->inputId()];
+        auto input = stateMachineInstance->input(condition->inputId());
 
         if ((ignoreTriggers && condition->is<TransitionTriggerCondition>()) ||
             !condition->evaluate(input))
diff --git a/src/animation/system_state_instance.cpp b/src/animation/system_state_instance.cpp
index 88e6194..92e28de 100644
--- a/src/animation/system_state_instance.cpp
+++ b/src/animation/system_state_instance.cpp
@@ -5,7 +5,7 @@
     StateInstance(layerState)
 {}
 
-void SystemStateInstance::advance(float seconds, Span<SMIInput*>) {}
+void SystemStateInstance::advance(float seconds, StateMachineInstance* stateMachineInstance) {}
 void SystemStateInstance::apply(float mix) {}
 
 bool SystemStateInstance::keepGoing() const { return false; }
\ No newline at end of file
diff --git a/src/event.cpp b/src/event.cpp
new file mode 100644
index 0000000..84be0ee
--- /dev/null
+++ b/src/event.cpp
@@ -0,0 +1,9 @@
+#include "rive/event.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+using namespace rive;
+
+void Event::trigger(const CallbackData& value)
+{
+    value.context()->reportEvent(this, value.delaySeconds());
+}
\ No newline at end of file
diff --git a/src/generated/animation/keyframe_callback_base.cpp b/src/generated/animation/keyframe_callback_base.cpp
new file mode 100644
index 0000000..d68eefb
--- /dev/null
+++ b/src/generated/animation/keyframe_callback_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_callback_base.hpp"
+#include "rive/animation/keyframe_callback.hpp"
+
+using namespace rive;
+
+Core* KeyFrameCallbackBase::clone() const
+{
+    auto cloned = new KeyFrameCallback();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_fire_event.cpp b/src/generated/animation/state_machine_fire_event.cpp
index f5f4d7b..9513eb7 100644
--- a/src/generated/animation/state_machine_fire_event.cpp
+++ b/src/generated/animation/state_machine_fire_event.cpp
@@ -26,5 +26,5 @@
     {
         return;
     }
-    stateMachineInstance->fireEvent(coreEvent->as<Event>());
+    stateMachineInstance->reportEvent(coreEvent->as<Event>());
 }
\ No newline at end of file
diff --git a/test/animation_state_instance_test.cpp b/test/animation_state_instance_test.cpp
index af648d4..29b8522 100644
--- a/test/animation_state_instance_test.cpp
+++ b/test/animation_state_instance_test.cpp
@@ -1,9 +1,10 @@
-#include <rive/animation/loop.hpp>
-#include <rive/animation/linear_animation.hpp>
-#include <rive/animation/linear_animation_instance.hpp>
+#include "rive/animation/loop.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
 
-#include <rive/animation/animation_state.hpp>
-#include <rive/animation/animation_state_instance.hpp>
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
 #include "utils/no_op_factory.hpp"
 #include "rive/scene.hpp"
 #include <catch.hpp>
@@ -30,8 +31,9 @@
         new rive::AnimationStateInstance(animationState, abi.get());
 
     // play from beginning.
-    std::vector<rive::SMIInput*> m_InputInstances;
-    animationStateInstance->advance(2.0, m_InputInstances);
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
 
     REQUIRE(animationStateInstance->animationInstance()->time() == 2.0);
     REQUIRE(animationStateInstance->animationInstance()->totalTime() == 2.0);
@@ -49,6 +51,9 @@
     rive::Artboard ab(&emptyFactory);
     auto abi = ab.instance();
 
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
     rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
     // duration in seconds is 5
     linearAnimation->duration(10);
@@ -63,8 +68,7 @@
         new rive::AnimationStateInstance(animationState, abi.get());
 
     // play from beginning.
-    std::vector<rive::SMIInput*> m_InputInstances;
-    animationStateInstance->advance(2.0, m_InputInstances);
+    animationStateInstance->advance(2.0, &stateMachineInstance);
 
     REQUIRE(animationStateInstance->animationInstance()->time() == 4.0);
     REQUIRE(animationStateInstance->animationInstance()->totalTime() == 4.0);
@@ -96,8 +100,9 @@
         new rive::AnimationStateInstance(animationState, abi.get());
 
     // play from beginning.
-    std::vector<rive::SMIInput*> m_InputInstances;
-    animationStateInstance->advance(2.0, m_InputInstances);
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
 
     REQUIRE(animationStateInstance->animationInstance()->time() == 1.0);
     REQUIRE(animationStateInstance->animationInstance()->totalTime() == 1.0);
@@ -129,8 +134,9 @@
         new rive::AnimationStateInstance(animationState, abi.get());
 
     // play from beginning.
-    std::vector<rive::SMIInput*> m_InputInstances;
-    animationStateInstance->advance(2.0, m_InputInstances);
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
 
     // backwards 2 seconds from 5.
     REQUIRE(animationStateInstance->animationInstance()->time() == 3.0);
diff --git a/test/assets/timeline_event_test.riv b/test/assets/timeline_event_test.riv
new file mode 100644
index 0000000..cec3239
--- /dev/null
+++ b/test/assets/timeline_event_test.riv
Binary files differ
diff --git a/test/state_machine_event_test.cpp b/test/state_machine_event_test.cpp
index a72e47d..b801a03 100644
--- a/test/state_machine_event_test.cpp
+++ b/test/state_machine_event_test.cpp
@@ -150,20 +150,20 @@
     REQUIRE(event->is<rive::Event>());
     REQUIRE(event->as<rive::Event>()->name() == "Footstep");
 
-    REQUIRE(stateMachineInstance->firedEventCount() == 0);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
     stateMachineInstance->pointerDown(rive::Vec2D(343.0f, 116.0f));
     stateMachineInstance->pointerUp(rive::Vec2D(343.0f, 116.0f));
 
     // There are two events on the listener.
-    REQUIRE(stateMachineInstance->firedEventCount() == 2);
-    auto firedEvent1 = stateMachineInstance->firedEventAt(0);
-    REQUIRE(firedEvent1->name() == "Footstep");
-    auto firedEvent2 = stateMachineInstance->firedEventAt(1);
-    REQUIRE(firedEvent2->name() == "Event 3");
+    REQUIRE(stateMachineInstance->reportedEventCount() == 2);
+    auto reportedEvent1 = stateMachineInstance->reportedEventAt(0);
+    REQUIRE(reportedEvent1.event()->name() == "Footstep");
+    auto reportedEvent2 = stateMachineInstance->reportedEventAt(1);
+    REQUIRE(reportedEvent2.event()->name() == "Event 3");
 
-    // After advancing again the firedEventCount should return to 0.
+    // After advancing again the reportedEventCount should return to 0.
     stateMachineInstance->advance(0.0f);
-    REQUIRE(stateMachineInstance->firedEventCount() == 0);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
 }
 
 TEST_CASE("events load correctly on a state and transition", "[events]")
@@ -197,21 +197,47 @@
     REQUIRE(transition->events().size() == 2);
 
     // First should've fired as we immediately went to Timeline 1.
-    REQUIRE(stateMachineInstance->firedEventCount() == 1);
-    REQUIRE(stateMachineInstance->firedEventAt(0)->name() == "First");
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "First");
 
     stateMachineInstance->advance(1.0f);
     // Exits after 2 seconds so 1 second in no events should've fired yet
-    REQUIRE(stateMachineInstance->firedEventCount() == 0);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
 
     stateMachineInstance->advance(1.0f);
     // At 2 seconds 2 events should fire, one for exiting the state and for taking the transition.
-    REQUIRE(stateMachineInstance->firedEventCount() == 2);
-    REQUIRE(stateMachineInstance->firedEventAt(0)->name() == "Second");
-    REQUIRE(stateMachineInstance->firedEventAt(1)->name() == "Third");
+    REQUIRE(stateMachineInstance->reportedEventCount() == 2);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Second");
+    REQUIRE(stateMachineInstance->reportedEventAt(1).event()->name() == "Third");
 
     stateMachineInstance->advance(1.0f);
     // Another second in the transition should complete
-    REQUIRE(stateMachineInstance->firedEventCount() == 1);
-    REQUIRE(stateMachineInstance->firedEventAt(0)->name() == "Fourth");
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Fourth");
+}
+
+TEST_CASE("timeline events load correctly and report", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/timeline_event_test.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachineInstance = artboard->stateMachineAt(0);
+    REQUIRE(stateMachineInstance != nullptr);
+
+    artboard->advance(0.0f);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+
+    stateMachineInstance->advance(0.4f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+
+    stateMachineInstance->advance(0.2f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Half");
+
+    // Event should've occurred right at 0.5 seconds.
+    REQUIRE(stateMachineInstance->reportedEventAt(0).secondsDelay() == Approx(0.1f));
 }