Choose events to fire on State and Transition start/end.

Allows selection of event and when to fire it per state and transition.

On a state:
![CleanShot 2023-08-16 at 22 28 32@2x](https://github.com/rive-app/rive/assets/454182/34c49212-6638-4187-91ed-d99042cc5a2a)

On a transition:
![CleanShot 2023-08-16 at 22 28 53@2x](https://github.com/rive-app/rive/assets/454182/d7bcbcbf-6719-4c5f-9a2a-cfee491ac718)

I also kept getting the annoying combo-box stuck at the bottom of the screen with these combos so low to the bottom right, so I fixed that too. Which helps the text combos too. Popups with predictable heights will try to open in the direction where they'll have most vertical space:
<img width="570" alt="CleanShot 2023-08-16 at 22 30 02@2x" src="https://github.com/rive-app/rive/assets/454182/88802e09-04df-4256-b4c1-2cc2bf490fcd">

Diffs=
ad4236501 Choose events to fire on State and Transition start/end. (#5830)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 498b00a..bb2edbd 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-abe5aab1431c34a42458a9650831d6b3a7c39feb
+ad4236501bdf0218046a93c54f2ee4a379747890
diff --git a/dev/defs/animation/state_machine_fire_event.json b/dev/defs/animation/state_machine_fire_event.json
new file mode 100644
index 0000000..063ff2c
--- /dev/null
+++ b/dev/defs/animation/state_machine_fire_event.json
@@ -0,0 +1,49 @@
+{
+    "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
+        },
+        "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/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp
index a086dce..1b7c91d 100644
--- a/include/rive/animation/linear_animation_instance.hpp
+++ b/include/rive/animation/linear_animation_instance.hpp
@@ -10,18 +10,6 @@
 
 class LinearAnimationInstance : public Scene
 {
-private:
-    const LinearAnimation* m_Animation = nullptr;
-    float m_Time;
-    float m_TotalTime;
-    float m_LastTotalTime;
-    float m_SpilledTime;
-
-    // float because it gets multiplied with other floats
-    float m_Direction;
-    bool m_DidLoop;
-    int m_LoopValue = -1;
-
 public:
     LinearAnimationInstance(const LinearAnimation*, ArtboardInstance*, float speedMultiplier = 1.0);
     LinearAnimationInstance(LinearAnimationInstance const&);
@@ -31,17 +19,17 @@
     // animation will continue to animate after this advance.
     bool advance(float seconds);
 
-    void clearSpilledTime() { m_SpilledTime = 0; }
+    void clearSpilledTime() { m_spilledTime = 0; }
 
     // Returns a pointer to the instance's animation
-    const LinearAnimation* animation() const { return m_Animation; }
+    const LinearAnimation* animation() const { return m_animation; }
 
     // Returns the current point in time at which this instance has advance
     // to
-    float time() const { return m_Time; }
+    float time() const { return m_time; }
 
     // Returns the direction that we are currently playing in
-    float direction() const { return m_Direction; }
+    float direction() const { return m_direction; }
 
     // Update the direction of the animation instance, positive value for
     // forwards Negative for backwards
@@ -49,11 +37,11 @@
     {
         if (direction > 0)
         {
-            m_Direction = 1;
+            m_direction = 1;
         }
         else
         {
-            m_Direction = -1;
+            m_direction = -1;
         }
     }
 
@@ -63,20 +51,20 @@
     // Applies the animation instance to its artboard instance. The mix (a value
     // between 0 and 1) is the strength at which the animation is mixed with
     // other animations applied to the artboard.
-    void apply(float mix = 1.0f) const { m_Animation->apply(m_ArtboardInstance, m_Time, mix); }
+    void apply(float mix = 1.0f) const { m_animation->apply(m_artboardInstance, m_time, mix); }
 
     // Set when the animation is advanced, true if the animation has stopped
     // (oneShot), reached the end (loop), or changed direction (pingPong)
-    bool didLoop() const { return m_DidLoop; }
+    bool didLoop() const { return m_didLoop; }
 
     bool keepGoing() const
     {
-        return m_LoopValue != static_cast<int>(rive::Loop::oneShot) || !m_DidLoop;
+        return m_loopValue != static_cast<int>(rive::Loop::oneShot) || !m_didLoop;
     }
 
-    float totalTime() const { return m_TotalTime; }
-    float lastTotalTime() const { return m_LastTotalTime; }
-    float spilledTime() const { return m_SpilledTime; }
+    float totalTime() const { return m_totalTime; }
+    float lastTotalTime() const { return m_lastTotalTime; }
+    float spilledTime() const { return m_spilledTime; }
     float durationSeconds() const override;
 
     // Forwarded from animation
@@ -95,6 +83,18 @@
     bool advanceAndApply(float seconds) override;
     std::string name() const override;
     void reset(float speedMultiplier);
+
+private:
+    const LinearAnimation* m_animation = nullptr;
+    float m_time;
+    float m_totalTime;
+    float m_lastTotalTime;
+    float m_spilledTime;
+
+    // float because it gets multiplied with other floats
+    float m_direction;
+    bool m_didLoop;
+    int m_loopValue = -1;
 };
 } // namespace rive
 #endif
diff --git a/include/rive/animation/state_machine_fire_event.hpp b/include/rive/animation/state_machine_fire_event.hpp
new file mode 100644
index 0000000..47526c9
--- /dev/null
+++ b/include/rive/animation/state_machine_fire_event.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_STATE_MACHINE_FIRE_EVENT_HPP_
+#define _RIVE_STATE_MACHINE_FIRE_EVENT_HPP_
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+
+namespace rive
+{
+class StateMachineInstance;
+enum class StateMachineFireOccurance : int
+{
+    atStart = 0,
+    atEnd = 1
+};
+
+class StateMachineFireEvent : public StateMachineFireEventBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    StateMachineFireOccurance occurs() const { return (StateMachineFireOccurance)occursValue(); }
+    void perform(StateMachineInstance* stateMachineInstance) const;
+};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index 8bd9877..e08e82d 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -28,18 +28,8 @@
     friend class SMIInput;
 
 private:
-    const StateMachine* m_Machine;
-    bool m_NeedsAdvance = false;
-
-    std::vector<SMIInput*> m_InputInstances; // we own each pointer
-    size_t m_LayerCount;
-    StateMachineLayerInstance* m_Layers;
-
     void markNeedsAdvance();
 
-    std::vector<std::unique_ptr<HitShape>> m_HitShapes;
-    std::vector<NestedArtboard*> m_HitNestedArtboards;
-
     /// Provide a hitListener if you want to process a down or an up for the pointer position
     /// too.
     void updateListeners(Vec2D position, ListenerType hitListener);
@@ -60,9 +50,9 @@
     bool needsAdvance() const;
 
     // Returns a pointer to the instance's stateMachine
-    const StateMachine* stateMachine() const { return m_Machine; }
+    const StateMachine* stateMachine() const { return m_machine; }
 
-    size_t inputCount() const override { return m_InputInstances.size(); }
+    size_t inputCount() const override { return m_inputInstances.size(); }
     SMIInput* input(size_t index) const override;
     SMIBool* getBool(const std::string& name) const override;
     SMINumber* getNumber(const std::string& name) const override;
@@ -92,7 +82,7 @@
 
     /// Allow anything referencing a concrete StateMachineInstace access to
     /// the backing artboard (explicitly not allowed on Scenes).
-    Artboard* artboard() { return m_ArtboardInstance; }
+    Artboard* artboard() { return m_artboardInstance; }
 
     /// Tracks an event that fired, will be cleared at the end of the next advance.
     void fireEvent(Event* event);
@@ -105,6 +95,13 @@
 
 private:
     std::vector<Event*> m_firedEvents;
+    const StateMachine* m_machine;
+    bool m_needsAdvance = false;
+    std::vector<SMIInput*> m_inputInstances; // we own each pointer
+    size_t m_layerCount;
+    StateMachineLayerInstance* m_layers;
+    std::vector<std::unique_ptr<HitShape>> m_hitShapes;
+    std::vector<NestedArtboard*> m_hitNestedArtboards;
 };
 } // namespace rive
 #endif
diff --git a/include/rive/animation/state_machine_layer_component.hpp b/include/rive/animation/state_machine_layer_component.hpp
index 4016d5e..e8c6799 100644
--- a/include/rive/animation/state_machine_layer_component.hpp
+++ b/include/rive/animation/state_machine_layer_component.hpp
@@ -1,12 +1,21 @@
 #ifndef _RIVE_STATE_MACHINE_LAYER_COMPONENT_HPP_
 #define _RIVE_STATE_MACHINE_LAYER_COMPONENT_HPP_
 #include "rive/generated/animation/state_machine_layer_component_base.hpp"
-#include <stdio.h>
+#include <vector>
+
 namespace rive
 {
+class StateMachineFireEvent;
 class StateMachineLayerComponent : public StateMachineLayerComponentBase
 {
+    friend class StateMachineLayerComponentImporter;
+
 public:
+    const std::vector<StateMachineFireEvent*>& events() const { return m_events; }
+    ~StateMachineLayerComponent() override;
+
+private:
+    std::vector<StateMachineFireEvent*> m_events;
 };
 } // namespace rive
 
diff --git a/include/rive/generated/animation/state_machine_fire_event_base.hpp b/include/rive/generated/animation/state_machine_fire_event_base.hpp
new file mode 100644
index 0000000..f9dd827
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_fire_event_base.hpp
@@ -0,0 +1,87 @@
+#ifndef _RIVE_STATE_MACHINE_FIRE_EVENT_BASE_HPP_
+#define _RIVE_STATE_MACHINE_FIRE_EVENT_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class StateMachineFireEventBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 169;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineFireEventBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t eventIdPropertyKey = 392;
+    static const uint16_t occursValuePropertyKey = 393;
+
+private:
+    uint32_t m_EventId = -1;
+    uint32_t m_OccursValue = 0;
+
+public:
+    inline uint32_t eventId() const { return m_EventId; }
+    void eventId(uint32_t value)
+    {
+        if (m_EventId == value)
+        {
+            return;
+        }
+        m_EventId = value;
+        eventIdChanged();
+    }
+
+    inline uint32_t occursValue() const { return m_OccursValue; }
+    void occursValue(uint32_t value)
+    {
+        if (m_OccursValue == value)
+        {
+            return;
+        }
+        m_OccursValue = value;
+        occursValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateMachineFireEventBase& object)
+    {
+        m_EventId = object.m_EventId;
+        m_OccursValue = object.m_OccursValue;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case eventIdPropertyKey:
+                m_EventId = CoreUintType::deserialize(reader);
+                return true;
+            case occursValuePropertyKey:
+                m_OccursValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void eventIdChanged() {}
+    virtual void occursValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index bf09d27..7984af9 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -45,6 +45,7 @@
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_machine_bool.hpp"
 #include "rive/animation/state_machine_component.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
 #include "rive/animation/state_machine_input.hpp"
 #include "rive/animation/state_machine_layer.hpp"
 #include "rive/animation/state_machine_layer_component.hpp"
@@ -231,6 +232,8 @@
                 return new KeyFrameColor();
             case StateMachineBase::typeKey:
                 return new StateMachine();
+            case StateMachineFireEventBase::typeKey:
+                return new StateMachineFireEvent();
             case EntryStateBase::typeKey:
                 return new EntryState();
             case LinearAnimationBase::typeKey:
@@ -511,6 +514,12 @@
             case StateTransitionBase::interpolatorIdPropertyKey:
                 object->as<StateTransitionBase>()->interpolatorId(value);
                 break;
+            case StateMachineFireEventBase::eventIdPropertyKey:
+                object->as<StateMachineFireEventBase>()->eventId(value);
+                break;
+            case StateMachineFireEventBase::occursValuePropertyKey:
+                object->as<StateMachineFireEventBase>()->occursValue(value);
+                break;
             case LinearAnimationBase::fpsPropertyKey:
                 object->as<LinearAnimationBase>()->fps(value);
                 break;
@@ -1242,6 +1251,10 @@
                 return object->as<StateTransitionBase>()->interpolationType();
             case StateTransitionBase::interpolatorIdPropertyKey:
                 return object->as<StateTransitionBase>()->interpolatorId();
+            case StateMachineFireEventBase::eventIdPropertyKey:
+                return object->as<StateMachineFireEventBase>()->eventId();
+            case StateMachineFireEventBase::occursValuePropertyKey:
+                return object->as<StateMachineFireEventBase>()->occursValue();
             case LinearAnimationBase::fpsPropertyKey:
                 return object->as<LinearAnimationBase>()->fps();
             case LinearAnimationBase::durationPropertyKey:
@@ -1722,6 +1735,8 @@
             case StateTransitionBase::exitTimePropertyKey:
             case StateTransitionBase::interpolationTypePropertyKey:
             case StateTransitionBase::interpolatorIdPropertyKey:
+            case StateMachineFireEventBase::eventIdPropertyKey:
+            case StateMachineFireEventBase::occursValuePropertyKey:
             case LinearAnimationBase::fpsPropertyKey:
             case LinearAnimationBase::durationPropertyKey:
             case LinearAnimationBase::loopValuePropertyKey:
diff --git a/include/rive/importers/state_machine_layer_component_importer.hpp b/include/rive/importers/state_machine_layer_component_importer.hpp
new file mode 100644
index 0000000..b9132a2
--- /dev/null
+++ b/include/rive/importers/state_machine_layer_component_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_COMPONENT_IMPORTER_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_COMPONENT_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateMachineLayerComponent;
+class StateMachineFireEvent;
+
+class StateMachineLayerComponentImporter : public ImportStackObject
+{
+public:
+    StateMachineLayerComponentImporter(StateMachineLayerComponent* component);
+
+    void addFireEvent(StateMachineFireEvent* fireEvent);
+
+private:
+    StateMachineLayerComponent* m_stateMachineLayerComponent;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp
index 20a9e6a..dd4d6ed 100644
--- a/include/rive/scene.hpp
+++ b/include/rive/scene.hpp
@@ -19,14 +19,12 @@
 class Scene
 {
 protected:
-    ArtboardInstance* m_ArtboardInstance;
-
     Scene(ArtboardInstance*);
 
 public:
     virtual ~Scene() {}
 
-    Scene(Scene const& lhs) : m_ArtboardInstance(lhs.m_ArtboardInstance) {}
+    Scene(Scene const& lhs) : m_artboardInstance(lhs.m_artboardInstance) {}
 
     float width() const;
     float height() const;
@@ -55,6 +53,9 @@
     virtual SMIBool* getBool(const std::string&) const;
     virtual SMINumber* getNumber(const std::string&) const;
     virtual SMITrigger* getTrigger(const std::string&) const;
+
+protected:
+    ArtboardInstance* m_artboardInstance;
 };
 
 } // namespace rive
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp
index 0e9ea17..ac527f8 100644
--- a/src/animation/linear_animation_instance.cpp
+++ b/src/animation/linear_animation_instance.cpp
@@ -11,26 +11,26 @@
                                                  ArtboardInstance* instance,
                                                  float speedMultiplier) :
     Scene(instance),
-    m_Animation((assert(animation != nullptr), animation)),
-    m_Time((speedMultiplier >= 0) ? animation->startTime() : animation->endTime()),
-    m_TotalTime(0.0f),
-    m_LastTotalTime(0.0f),
-    m_SpilledTime(0.0f),
-    m_Direction(1)
+    m_animation((assert(animation != nullptr), animation)),
+    m_time((speedMultiplier >= 0) ? animation->startTime() : animation->endTime()),
+    m_totalTime(0.0f),
+    m_lastTotalTime(0.0f),
+    m_spilledTime(0.0f),
+    m_direction(1)
 {
     Counter::update(Counter::kLinearAnimationInstance, +1);
 }
 
 LinearAnimationInstance::LinearAnimationInstance(LinearAnimationInstance const& lhs) :
     Scene(lhs),
-    m_Animation(lhs.m_Animation),
-    m_Time(lhs.m_Time),
-    m_TotalTime(lhs.m_TotalTime),
-    m_LastTotalTime(lhs.m_LastTotalTime),
-    m_SpilledTime(lhs.m_SpilledTime),
-    m_Direction(lhs.m_Direction),
-    m_DidLoop(lhs.m_DidLoop),
-    m_LoopValue(lhs.m_LoopValue)
+    m_animation(lhs.m_animation),
+    m_time(lhs.m_time),
+    m_totalTime(lhs.m_totalTime),
+    m_lastTotalTime(lhs.m_lastTotalTime),
+    m_spilledTime(lhs.m_spilledTime),
+    m_direction(lhs.m_direction),
+    m_didLoop(lhs.m_didLoop),
+    m_loopValue(lhs.m_loopValue)
 {
     Counter::update(Counter::kLinearAnimationInstance, +1);
 }
@@ -44,30 +44,30 @@
 {
     bool more = this->advance(seconds);
     this->apply();
-    m_ArtboardInstance->advance(seconds);
+    m_artboardInstance->advance(seconds);
     return more;
 }
 
 bool LinearAnimationInstance::advance(float elapsedSeconds)
 {
-    const LinearAnimation& animation = *m_Animation;
-    float deltaSeconds = elapsedSeconds * animation.speed() * m_Direction;
+    const LinearAnimation& animation = *m_animation;
+    float deltaSeconds = elapsedSeconds * animation.speed() * m_direction;
     if (deltaSeconds == 0)
     {
         // we say keep going, if you advance by 0.
         // could argue that any further advances by 0 result in nothing so you should not keep going
         // could argue its saying, we are not at the end of the animation yet, so keep going
         // our runtimes currently expect the latter, so we say keep going!
-        m_DidLoop = false;
+        m_didLoop = false;
         return true;
     }
 
-    m_LastTotalTime = m_TotalTime;
-    m_TotalTime += std::abs(deltaSeconds);
-    m_Time += deltaSeconds;
+    m_lastTotalTime = m_totalTime;
+    m_totalTime += std::abs(deltaSeconds);
+    m_time += deltaSeconds;
 
     int fps = animation.fps();
-    float frames = m_Time * fps;
+    float frames = m_time * fps;
 
     int start = animation.enableWorkArea() ? animation.workStart() : 0;
     int end = animation.enableWorkArea() ? animation.workEnd() : animation.duration();
@@ -75,7 +75,7 @@
 
     bool keepGoing = true;
     bool didLoop = false;
-    m_SpilledTime = 0.0f;
+    m_spilledTime = 0.0f;
 
     // this has some issues when deltaSeconds is 0,
     // right now we basically assume we default to going forwards in that case
@@ -87,36 +87,36 @@
             if (direction == 1 && frames > end)
             {
                 keepGoing = false;
-                m_SpilledTime = (frames - end) / fps;
+                m_spilledTime = (frames - end) / fps;
                 frames = (float)end;
-                m_Time = frames / fps;
+                m_time = frames / fps;
                 didLoop = true;
             }
             else if (direction == -1 && frames < start)
             {
                 keepGoing = false;
-                m_SpilledTime = (start - frames) / fps;
+                m_spilledTime = (start - frames) / fps;
                 frames = (float)start;
-                m_Time = frames / fps;
+                m_time = frames / fps;
                 didLoop = true;
             }
             break;
         case Loop::loop:
             if (direction == 1 && frames >= end)
             {
-                m_SpilledTime = (frames - end) / fps;
-                frames = m_Time * fps;
+                m_spilledTime = (frames - end) / fps;
+                frames = m_time * fps;
                 frames = start + std::fmod(frames - start, (float)range);
 
-                m_Time = frames / fps;
+                m_time = frames / fps;
                 didLoop = true;
             }
             else if (direction == -1 && frames <= start)
             {
-                m_SpilledTime = (start - frames) / fps;
-                frames = m_Time * fps;
+                m_spilledTime = (start - frames) / fps;
+                frames = m_time * fps;
                 frames = end - std::abs(std::fmod(start - frames, (float)range));
-                m_Time = frames / fps;
+                m_time = frames / fps;
                 didLoop = true;
             }
             break;
@@ -125,12 +125,12 @@
             {
                 if (direction == 1 && frames >= end)
                 {
-                    m_SpilledTime = (frames - end) / fps;
+                    m_spilledTime = (frames - end) / fps;
                     frames = end + (end - frames);
                 }
                 else if (direction == -1 && frames < start)
                 {
-                    m_SpilledTime = (start - frames) / fps;
+                    m_spilledTime = (start - frames) / fps;
                     frames = start + (start - frames);
                 }
                 else
@@ -142,80 +142,80 @@
                     // where animations are not advanced on regular intervals.
                     break;
                 }
-                m_Time = frames / fps;
-                m_Direction *= -1;
+                m_time = frames / fps;
+                m_direction *= -1;
                 direction *= -1;
                 didLoop = true;
             }
             break;
     }
 
-    m_DidLoop = didLoop;
+    m_didLoop = didLoop;
     return keepGoing;
 }
 
 void LinearAnimationInstance::time(float value)
 {
-    if (m_Time == value)
+    if (m_time == value)
     {
         return;
     }
-    m_Time = value;
+    m_time = value;
     // Make sure to keep last and total in relative lockstep so state machines
     // can track change even when setting time.
-    auto diff = m_TotalTime - m_LastTotalTime;
+    auto diff = m_totalTime - m_lastTotalTime;
 
-    int start = (m_Animation->enableWorkArea() ? m_Animation->workStart() : 0) * m_Animation->fps();
-    m_TotalTime = value - start;
-    m_LastTotalTime = m_TotalTime - diff;
+    int start = (m_animation->enableWorkArea() ? m_animation->workStart() : 0) * m_animation->fps();
+    m_totalTime = value - start;
+    m_lastTotalTime = m_totalTime - diff;
 
     // leaving this RIGHT now. but is this required? it kinda messes up
     // playing things backwards and seeking. what purpose does it solve?
-    m_Direction = 1;
+    m_direction = 1;
 }
 
 void LinearAnimationInstance::reset(float speedMultiplier = 1.0)
 {
-    m_Time = (speedMultiplier >= 0) ? m_Animation->startTime() : m_Animation->endTime();
+    m_time = (speedMultiplier >= 0) ? m_animation->startTime() : m_animation->endTime();
 }
 
-uint32_t LinearAnimationInstance::fps() const { return m_Animation->fps(); }
+uint32_t LinearAnimationInstance::fps() const { return m_animation->fps(); }
 
-uint32_t LinearAnimationInstance::duration() const { return m_Animation->duration(); }
+uint32_t LinearAnimationInstance::duration() const { return m_animation->duration(); }
 
-float LinearAnimationInstance::speed() const { return m_Animation->speed(); }
+float LinearAnimationInstance::speed() const { return m_animation->speed(); }
 
-float LinearAnimationInstance::startTime() const { return m_Animation->startTime(); }
+float LinearAnimationInstance::startTime() const { return m_animation->startTime(); }
 
-std::string LinearAnimationInstance::name() const { return m_Animation->name(); }
+std::string LinearAnimationInstance::name() const { return m_animation->name(); }
 
 bool LinearAnimationInstance::isTranslucent() const
 {
-    return m_ArtboardInstance->isTranslucent(this);
+    return m_artboardInstance->isTranslucent(this);
 }
 
 // Returns either the animation's default or overridden loop values
 int LinearAnimationInstance::loopValue() const
 {
-    if (m_LoopValue != -1)
+    if (m_loopValue != -1)
     {
-        return m_LoopValue;
+        return m_loopValue;
     }
-    return m_Animation->loopValue();
+    return m_animation->loopValue();
 }
 
 // Override the animation's loop value
 void LinearAnimationInstance::loopValue(int value)
 {
-    if (m_LoopValue == value)
+    if (m_loopValue == value)
     {
         return;
     }
-    if (m_LoopValue == -1 && m_Animation->loopValue() == value)
+    if (m_loopValue == -1 && m_animation->loopValue() == value)
     {
         return;
     }
-    m_LoopValue = value;
+    m_loopValue = value;
 }
 
-float LinearAnimationInstance::durationSeconds() const { return m_Animation->durationSeconds(); }
+float LinearAnimationInstance::durationSeconds() const { return m_animation->durationSeconds(); }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index cbccf6d..1993115 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -16,6 +16,7 @@
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_transition.hpp"
 #include "rive/animation/transition_condition.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
 #include "rive/math/aabb.hpp"
 #include "rive/math/hit_test.hpp"
 #include "rive/nested_animation.hpp"
@@ -29,78 +30,61 @@
 {
 class StateMachineLayerInstance
 {
-private:
-    static const int maxIterations = 100;
-    const StateMachineLayer* m_Layer = nullptr;
-    ArtboardInstance* m_ArtboardInstance = nullptr;
-
-    StateInstance* m_AnyStateInstance = nullptr;
-    StateInstance* m_CurrentState = nullptr;
-    StateInstance* m_StateFrom = nullptr;
-
-    // const LayerState* m_CurrentState = nullptr;
-    // const LayerState* m_StateFrom = nullptr;
-    const StateTransition* m_Transition = nullptr;
-
-    bool m_HoldAnimationFrom = false;
-    // LinearAnimationInstance* m_AnimationInstance = nullptr;
-    // LinearAnimationInstance* m_AnimationInstanceFrom = nullptr;
-    float m_Mix = 1.0f;
-    float m_MixFrom = 1.0f;
-    bool m_StateChangedOnAdvance = false;
-
-    bool m_WaitingForExit = false;
-    /// Used to ensure a specific animation is applied on the next apply.
-    const LinearAnimation* m_HoldAnimation = nullptr;
-    float m_HoldTime = 0.0f;
-
 public:
     ~StateMachineLayerInstance()
     {
-        delete m_AnyStateInstance;
-        delete m_CurrentState;
-        delete m_StateFrom;
+        delete m_anyStateInstance;
+        delete m_currentState;
+        delete m_stateFrom;
     }
 
-    void init(const StateMachineLayer* layer, ArtboardInstance* instance)
+    void init(StateMachineInstance* stateMachineInstance,
+              const StateMachineLayer* layer,
+              ArtboardInstance* instance)
     {
-        m_ArtboardInstance = instance;
-        assert(m_Layer == nullptr);
-        m_AnyStateInstance = layer->anyState()->makeInstance(instance).release();
-        m_Layer = layer;
-        changeState(m_Layer->entryState());
+        m_stateMachineInstance = stateMachineInstance;
+        m_artboardInstance = instance;
+        assert(m_layer == nullptr);
+        m_anyStateInstance = layer->anyState()->makeInstance(instance).release();
+        m_layer = layer;
+        changeState(m_layer->entryState());
     }
 
     void updateMix(float seconds)
     {
-        if (m_Transition != nullptr && m_StateFrom != nullptr && m_Transition->duration() != 0)
+        if (m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0)
         {
-            m_Mix = std::min(
+            m_mix = std::min(
                 1.0f,
-                std::max(0.0f, (m_Mix + seconds / m_Transition->mixTime(m_StateFrom->state()))));
+                std::max(0.0f, (m_mix + seconds / m_transition->mixTime(m_stateFrom->state()))));
+            if (m_mix == 1.0f && !m_transitionCompleted)
+            {
+                m_transitionCompleted = true;
+                fireEvents(StateMachineFireOccurance::atEnd, m_transition->events());
+            }
         }
         else
         {
-            m_Mix = 1.0f;
+            m_mix = 1.0f;
         }
     }
 
     bool advance(float seconds, Span<SMIInput*> inputs)
     {
-        m_StateChangedOnAdvance = false;
+        m_stateMachineChangedOnAdvance = false;
 
-        if (m_CurrentState != nullptr && m_CurrentState->keepGoing())
+        if (m_currentState != nullptr && m_currentState->keepGoing())
         {
-            m_CurrentState->advance(seconds, inputs);
+            m_currentState->advance(seconds, inputs);
         }
 
         updateMix(seconds);
 
-        if (m_StateFrom != nullptr && m_Mix < 1.0f && !m_HoldAnimationFrom)
+        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, inputs);
         }
 
         for (int i = 0; updateState(inputs, i != 0); i++)
@@ -116,16 +100,16 @@
 
         apply();
 
-        m_CurrentState->clearSpilledTime();
+        m_currentState->clearSpilledTime();
 
-        return m_Mix != 1.0f || m_WaitingForExit ||
-               (m_CurrentState != nullptr && m_CurrentState->keepGoing());
+        return m_mix != 1.0f || m_waitingForExit ||
+               (m_currentState != nullptr && m_currentState->keepGoing());
     }
 
     bool isTransitioning()
     {
-        return m_Transition != nullptr && m_StateFrom != nullptr && m_Transition->duration() != 0 &&
-               m_Mix < 1.0f;
+        return m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0 &&
+               m_mix < 1.0f;
     }
 
     bool updateState(Span<SMIInput*> inputs, bool ignoreTriggers)
@@ -137,24 +121,49 @@
             return false;
         }
 
-        m_WaitingForExit = false;
+        m_waitingForExit = false;
 
-        if (tryChangeState(m_AnyStateInstance, inputs, ignoreTriggers))
+        if (tryChangeState(m_anyStateInstance, inputs, ignoreTriggers))
         {
             return true;
         }
 
-        return tryChangeState(m_CurrentState, inputs, ignoreTriggers);
+        return tryChangeState(m_currentState, inputs, ignoreTriggers);
+    }
+
+    void fireEvents(StateMachineFireOccurance occurs,
+                    const std::vector<StateMachineFireEvent*>& fireEvents)
+    {
+        for (auto event : fireEvents)
+        {
+            if (event->occurs() == occurs)
+            {
+                event->perform(m_stateMachineInstance);
+            }
+        }
     }
 
     bool changeState(const LayerState* stateTo)
     {
-        if ((m_CurrentState == nullptr ? nullptr : m_CurrentState->state()) == stateTo)
+        if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == stateTo)
         {
             return false;
         }
-        m_CurrentState =
-            stateTo == nullptr ? nullptr : stateTo->makeInstance(m_ArtboardInstance).release();
+
+        // Fire end events for the state we're changing from.
+        if (m_currentState != nullptr)
+        {
+            fireEvents(StateMachineFireOccurance::atEnd, m_currentState->state()->events());
+        }
+
+        m_currentState =
+            stateTo == nullptr ? nullptr : stateTo->makeInstance(m_artboardInstance).release();
+
+        // Fire start events for the state we're changing to.
+        if (m_currentState != nullptr)
+        {
+            fireEvents(StateMachineFireOccurance::atStart, m_currentState->state()->events());
+        }
         return true;
     }
 
@@ -167,22 +176,33 @@
             return false;
         }
         auto stateFrom = stateFromInstance->state();
-        auto outState = m_CurrentState;
+        auto outState = m_currentState;
         for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
         {
             auto transition = stateFrom->transition(i);
             auto allowed = transition->allowed(stateFromInstance, inputs, ignoreTriggers);
             if (allowed == AllowTransition::yes && changeState(transition->stateTo()))
             {
-                m_StateChangedOnAdvance = true;
+                m_stateMachineChangedOnAdvance = true;
                 // state actually has changed
-                m_Transition = transition;
-                if (m_StateFrom != m_AnyStateInstance)
+                m_transition = transition;
+                fireEvents(StateMachineFireOccurance::atStart, transition->events());
+                if (transition->duration() == 0)
+                {
+                    m_transitionCompleted = true;
+                    fireEvents(StateMachineFireOccurance::atEnd, transition->events());
+                }
+                else
+                {
+                    m_transitionCompleted = false;
+                }
+
+                if (m_stateFrom != m_anyStateInstance)
                 {
                     // Old state from is done.
-                    delete m_StateFrom;
+                    delete m_stateFrom;
                 }
-                m_StateFrom = outState;
+                m_stateFrom = outState;
 
                 // If we had an exit time and wanted to pause on exit, make
                 // sure to hold the exit time. Delegate this to the
@@ -192,35 +212,35 @@
                     // Make sure we apply this state. This only returns true
                     // when it's an animation state instance.
                     auto instance =
-                        static_cast<AnimationStateInstance*>(m_StateFrom)->animationInstance();
+                        static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
 
-                    m_HoldAnimation = instance->animation();
-                    m_HoldTime = instance->time();
+                    m_holdAnimation = instance->animation();
+                    m_holdTime = instance->time();
                 }
-                m_MixFrom = m_Mix;
+                m_mixFrom = m_mix;
 
                 // Keep mixing last animation that was mixed in.
-                if (m_Mix != 0.0f)
+                if (m_mix != 0.0f)
                 {
-                    m_HoldAnimationFrom = transition->pauseOnExit();
+                    m_holdAnimationFrom = transition->pauseOnExit();
                 }
-                if (m_StateFrom != nullptr && m_StateFrom->state()->is<AnimationState>() &&
-                    m_CurrentState != nullptr)
+                if (m_stateFrom != nullptr && m_stateFrom->state()->is<AnimationState>() &&
+                    m_currentState != nullptr)
                 {
                     auto instance =
-                        static_cast<AnimationStateInstance*>(m_StateFrom)->animationInstance();
+                        static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
 
                     auto spilledTime = instance->spilledTime();
-                    m_CurrentState->advance(spilledTime, inputs);
+                    m_currentState->advance(spilledTime, inputs);
                 }
-                m_Mix = 0.0f;
+                m_mix = 0.0f;
                 updateMix(0.0f);
-                m_WaitingForExit = false;
+                m_waitingForExit = false;
                 return true;
             }
             else if (allowed == AllowTransition::waitingForExit)
             {
-                m_WaitingForExit = true;
+                m_waitingForExit = true;
             }
         }
         return false;
@@ -228,45 +248,69 @@
 
     void apply(/*Artboard* artboard*/)
     {
-        if (m_HoldAnimation != nullptr)
+        if (m_holdAnimation != nullptr)
         {
-            m_HoldAnimation->apply(m_ArtboardInstance, m_HoldTime, m_MixFrom);
-            m_HoldAnimation = nullptr;
+            m_holdAnimation->apply(m_artboardInstance, m_holdTime, m_mixFrom);
+            m_holdAnimation = nullptr;
         }
 
         CubicInterpolator* cubic = nullptr;
-        if (m_Transition != nullptr && m_Transition->interpolator() != nullptr)
+        if (m_transition != nullptr && m_transition->interpolator() != nullptr)
         {
-            cubic = m_Transition->interpolator();
+            cubic = m_transition->interpolator();
         }
 
-        if (m_StateFrom != nullptr && m_Mix < 1.0f)
+        if (m_stateFrom != nullptr && m_mix < 1.0f)
         {
-            auto fromMix = cubic != nullptr ? cubic->transform(m_MixFrom) : m_MixFrom;
-            m_StateFrom->apply(fromMix);
+            auto fromMix = cubic != nullptr ? cubic->transform(m_mixFrom) : m_mixFrom;
+            m_stateFrom->apply(fromMix);
         }
-        if (m_CurrentState != nullptr)
+        if (m_currentState != nullptr)
         {
-            auto mix = cubic != nullptr ? cubic->transform(m_Mix) : m_Mix;
-            m_CurrentState->apply(mix);
+            auto mix = cubic != nullptr ? cubic->transform(m_mix) : m_mix;
+            m_currentState->apply(mix);
         }
     }
 
-    bool stateChangedOnAdvance() const { return m_StateChangedOnAdvance; }
+    bool stateChangedOnAdvance() const { return m_stateMachineChangedOnAdvance; }
 
     const LayerState* currentState()
     {
-        return m_CurrentState == nullptr ? nullptr : m_CurrentState->state();
+        return m_currentState == nullptr ? nullptr : m_currentState->state();
     }
 
     const LinearAnimationInstance* currentAnimation() const
     {
-        if (m_CurrentState == nullptr || !m_CurrentState->state()->is<AnimationState>())
+        if (m_currentState == nullptr || !m_currentState->state()->is<AnimationState>())
         {
             return nullptr;
         }
-        return static_cast<AnimationStateInstance*>(m_CurrentState)->animationInstance();
+        return static_cast<AnimationStateInstance*>(m_currentState)->animationInstance();
     }
+
+private:
+    static const int maxIterations = 100;
+    StateMachineInstance* m_stateMachineInstance = nullptr;
+    const StateMachineLayer* m_layer = nullptr;
+    ArtboardInstance* m_artboardInstance = nullptr;
+
+    StateInstance* m_anyStateInstance = nullptr;
+    StateInstance* m_currentState = nullptr;
+    StateInstance* m_stateFrom = nullptr;
+
+    const StateTransition* m_transition = nullptr;
+    bool m_transitionCompleted = false;
+
+    bool m_holdAnimationFrom = false;
+
+    float m_mix = 1.0f;
+    float m_mixFrom = 1.0f;
+    bool m_stateMachineChangedOnAdvance = false;
+
+    bool m_waitingForExit = false;
+    /// Used to ensure a specific animation is applied on the next apply.
+    const LinearAnimation* m_holdAnimation = nullptr;
+    float m_holdTime = 0.0f;
 };
 
 /// Representation of a Shape from the Artboard Instance and all the listeners it
@@ -274,23 +318,23 @@
 /// shapes that trigger multiple listeners.
 class HitShape
 {
-private:
-    Shape* m_Shape;
-
 public:
-    Shape* shape() const { return m_Shape; }
-    HitShape(Shape* shape) : m_Shape(shape) {}
+    Shape* shape() const { return m_shape; }
+    HitShape(Shape* shape) : m_shape(shape) {}
     bool isHovered = false;
     std::vector<const StateMachineListener*> listeners;
+
+private:
+    Shape* m_shape;
 };
 } // namespace rive
 
 void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
 {
-    if (m_ArtboardInstance->frameOrigin())
+    if (m_artboardInstance->frameOrigin())
     {
-        position -= Vec2D(m_ArtboardInstance->originX() * m_ArtboardInstance->width(),
-                          m_ArtboardInstance->originY() * m_ArtboardInstance->height());
+        position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
+                          m_artboardInstance->originY() * m_artboardInstance->height());
     }
 
     const float hitRadius = 2;
@@ -300,7 +344,7 @@
                         position.y + hitRadius)
                        .round();
 
-    for (const auto& hitShape : m_HitShapes)
+    for (const auto& hitShape : m_hitShapes)
     {
 
         // TODO: quick reject.
@@ -340,7 +384,7 @@
     // can be sorted by drawOrder so they can be iterated in one loop and early
     // out if any hit stops propagation (also require the ability to mark a hit
     // as able to stop propagation)
-    for (auto nestedArtboard : m_HitNestedArtboards)
+    for (auto nestedArtboard : m_hitNestedArtboards)
     {
         Vec2D nestedPosition;
         if (!nestedArtboard->worldToLocal(position, &nestedPosition))
@@ -389,12 +433,12 @@
 
 StateMachineInstance::StateMachineInstance(const StateMachine* machine,
                                            ArtboardInstance* instance) :
-    Scene(instance), m_Machine(machine)
+    Scene(instance), m_machine(machine)
 {
     Counter::update(Counter::kStateMachineInstance, +1);
 
     const auto count = machine->inputCount();
-    m_InputInstances.resize(count);
+    m_inputInstances.resize(count);
     for (size_t i = 0; i < count; i++)
     {
         auto input = machine->input(i);
@@ -405,13 +449,13 @@
         switch (input->coreType())
         {
             case StateMachineBool::typeKey:
-                m_InputInstances[i] = new SMIBool(input->as<StateMachineBool>(), this);
+                m_inputInstances[i] = new SMIBool(input->as<StateMachineBool>(), this);
                 break;
             case StateMachineNumber::typeKey:
-                m_InputInstances[i] = new SMINumber(input->as<StateMachineNumber>(), this);
+                m_inputInstances[i] = new SMINumber(input->as<StateMachineNumber>(), this);
                 break;
             case StateMachineTrigger::typeKey:
-                m_InputInstances[i] = new SMITrigger(input->as<StateMachineTrigger>(), this);
+                m_inputInstances[i] = new SMITrigger(input->as<StateMachineTrigger>(), this);
                 break;
             default:
                 // Sanity check.
@@ -419,11 +463,11 @@
         }
     }
 
-    m_LayerCount = machine->layerCount();
-    m_Layers = new StateMachineLayerInstance[m_LayerCount];
-    for (size_t i = 0; i < m_LayerCount; i++)
+    m_layerCount = machine->layerCount();
+    m_layers = new StateMachineLayerInstance[m_layerCount];
+    for (size_t i = 0; i < m_layerCount; i++)
     {
-        m_Layers[i].init(machine->layer(i), m_ArtboardInstance);
+        m_layers[i].init(this, machine->layer(i), m_artboardInstance);
     }
 
     // Initialize listeners. Store a lookup table of shape id to hit shape
@@ -442,12 +486,12 @@
             auto itr = hitShapeLookup.find(id);
             if (itr == hitShapeLookup.end())
             {
-                auto shape = m_ArtboardInstance->resolve(id);
+                auto shape = m_artboardInstance->resolve(id);
                 if (shape != nullptr && shape->is<Shape>())
                 {
                     auto hs = rivestd::make_unique<HitShape>(shape->as<Shape>());
                     hitShapeLookup[id] = hitShape = hs.get();
-                    m_HitShapes.push_back(std::move(hs));
+                    m_hitShapes.push_back(std::move(hs));
                 }
                 else
                 {
@@ -467,18 +511,18 @@
     {
         if (nestedArtboard->hasNestedStateMachines())
         {
-            m_HitNestedArtboards.push_back(nestedArtboard);
+            m_hitNestedArtboards.push_back(nestedArtboard);
         }
     }
 }
 
 StateMachineInstance::~StateMachineInstance()
 {
-    for (auto inst : m_InputInstances)
+    for (auto inst : m_inputInstances)
     {
         delete inst;
     }
-    delete[] m_Layers;
+    delete[] m_layers;
 
     Counter::update(Counter::kStateMachineInstance, -1);
 }
@@ -486,40 +530,40 @@
 bool StateMachineInstance::advance(float seconds)
 {
     m_firedEvents.clear();
-    m_NeedsAdvance = false;
-    for (size_t i = 0; i < m_LayerCount; i++)
+    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_inputInstances))
         {
-            m_NeedsAdvance = true;
+            m_needsAdvance = true;
         }
     }
 
-    for (auto inst : m_InputInstances)
+    for (auto inst : m_inputInstances)
     {
         inst->advanced();
     }
 
-    return m_NeedsAdvance;
+    return m_needsAdvance;
 }
 
 bool StateMachineInstance::advanceAndApply(float seconds)
 {
     bool more = this->advance(seconds);
-    m_ArtboardInstance->advance(seconds);
+    m_artboardInstance->advance(seconds);
     return more;
 }
 
-void StateMachineInstance::markNeedsAdvance() { m_NeedsAdvance = true; }
-bool StateMachineInstance::needsAdvance() const { return m_NeedsAdvance; }
+void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; }
+bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; }
 
-std::string StateMachineInstance::name() const { return m_Machine->name(); }
+std::string StateMachineInstance::name() const { return m_machine->name(); }
 
 SMIInput* StateMachineInstance::input(size_t index) const
 {
-    if (index < m_InputInstances.size())
+    if (index < m_inputInstances.size())
     {
-        return m_InputInstances[index];
+        return m_inputInstances[index];
     }
     return nullptr;
 }
@@ -527,7 +571,7 @@
 template <typename SMType, typename InstType>
 InstType* StateMachineInstance::getNamedInput(const std::string& name) const
 {
-    for (const auto inst : m_InputInstances)
+    for (const auto inst : m_inputInstances)
     {
         auto input = inst->input();
         if (input->is<SMType>() && input->name() == name)
@@ -554,9 +598,9 @@
 size_t StateMachineInstance::stateChangedCount() const
 {
     size_t count = 0;
-    for (size_t i = 0; i < m_LayerCount; i++)
+    for (size_t i = 0; i < m_layerCount; i++)
     {
-        if (m_Layers[i].stateChangedOnAdvance())
+        if (m_layers[i].stateChangedOnAdvance())
         {
             count++;
         }
@@ -567,13 +611,13 @@
 const LayerState* StateMachineInstance::stateChangedByIndex(size_t index) const
 {
     size_t count = 0;
-    for (size_t i = 0; i < m_LayerCount; i++)
+    for (size_t i = 0; i < m_layerCount; i++)
     {
-        if (m_Layers[i].stateChangedOnAdvance())
+        if (m_layers[i].stateChangedOnAdvance())
         {
             if (count == index)
             {
-                return m_Layers[i].currentState();
+                return m_layers[i].currentState();
             }
             count++;
         }
@@ -584,9 +628,9 @@
 size_t StateMachineInstance::currentAnimationCount() const
 {
     size_t count = 0;
-    for (size_t i = 0; i < m_LayerCount; i++)
+    for (size_t i = 0; i < m_layerCount; i++)
     {
-        if (m_Layers[i].currentAnimation() != nullptr)
+        if (m_layers[i].currentAnimation() != nullptr)
         {
             count++;
         }
@@ -597,13 +641,13 @@
 const LinearAnimationInstance* StateMachineInstance::currentAnimationByIndex(size_t index) const
 {
     size_t count = 0;
-    for (size_t i = 0; i < m_LayerCount; i++)
+    for (size_t i = 0; i < m_layerCount; i++)
     {
-        if (m_Layers[i].currentAnimation() != nullptr)
+        if (m_layers[i].currentAnimation() != nullptr)
         {
             if (count == index)
             {
-                return m_Layers[i].currentAnimation();
+                return m_layers[i].currentAnimation();
             }
             count++;
         }
diff --git a/src/file.cpp b/src/file.cpp
index 2aea9cc..19c9a26 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -19,6 +19,7 @@
 #include "rive/importers/state_machine_layer_importer.hpp"
 #include "rive/importers/layer_state_importer.hpp"
 #include "rive/importers/state_transition_importer.hpp"
+#include "rive/importers/state_machine_layer_component_importer.hpp"
 #include "rive/animation/blend_state_transition.hpp"
 #include "rive/animation/any_state.hpp"
 #include "rive/animation/entry_state.hpp"
@@ -287,6 +288,13 @@
             // Some previous stack item didn't resolve.
             return ImportResult::malformed;
         }
+        if (object->is<StateMachineLayerComponent>() &&
+            importStack.makeLatest(StateMachineLayerComponent::typeKey,
+                                   new StateMachineLayerComponentImporter(
+                                       object->as<StateMachineLayerComponent>())) != StatusCode::Ok)
+        {
+            return ImportResult::malformed;
+        }
     }
 
     return !reader.hasError() && importStack.resolve() == StatusCode::Ok ? ImportResult::success
diff --git a/src/generated/animation/state_machine_fire_event.cpp b/src/generated/animation/state_machine_fire_event.cpp
new file mode 100644
index 0000000..f5f4d7b
--- /dev/null
+++ b/src/generated/animation/state_machine_fire_event.cpp
@@ -0,0 +1,30 @@
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/event.hpp"
+#include "rive/importers/state_machine_layer_component_importer.hpp"
+
+using namespace rive;
+
+StatusCode StateMachineFireEvent::import(ImportStack& importStack)
+{
+    auto stateImporter =
+        importStack.latest<StateMachineLayerComponentImporter>(StateMachineLayerComponent::typeKey);
+    if (stateImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    stateImporter->addFireEvent(this);
+    return Super::import(importStack);
+}
+
+void StateMachineFireEvent::perform(StateMachineInstance* stateMachineInstance) const
+{
+    auto coreEvent = stateMachineInstance->artboard()->resolve(eventId());
+    if (coreEvent == nullptr || !coreEvent->is<Event>())
+    {
+        return;
+    }
+    stateMachineInstance->fireEvent(coreEvent->as<Event>());
+}
\ No newline at end of file
diff --git a/src/generated/animation/state_machine_fire_event_base.cpp b/src/generated/animation/state_machine_fire_event_base.cpp
new file mode 100644
index 0000000..b43b331
--- /dev/null
+++ b/src/generated/animation/state_machine_fire_event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+
+using namespace rive;
+
+Core* StateMachineFireEventBase::clone() const
+{
+    auto cloned = new StateMachineFireEvent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/importers/state_machine_layer_component_importer.cpp b/src/importers/state_machine_layer_component_importer.cpp
new file mode 100644
index 0000000..d8e4c6d
--- /dev/null
+++ b/src/importers/state_machine_layer_component_importer.cpp
@@ -0,0 +1,23 @@
+#include "rive/importers/state_machine_layer_component_importer.hpp"
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+
+using namespace rive;
+
+StateMachineLayerComponent::~StateMachineLayerComponent()
+{
+    for (auto event : m_events)
+    {
+        delete event;
+    }
+}
+
+StateMachineLayerComponentImporter::StateMachineLayerComponentImporter(
+    StateMachineLayerComponent* component) :
+    m_stateMachineLayerComponent(component)
+{}
+
+void StateMachineLayerComponentImporter::addFireEvent(StateMachineFireEvent* fireEvent)
+{
+    m_stateMachineLayerComponent->m_events.push_back(fireEvent);
+}
\ No newline at end of file
diff --git a/src/scene.cpp b/src/scene.cpp
index b5f6467..e568553 100644
--- a/src/scene.cpp
+++ b/src/scene.cpp
@@ -3,16 +3,16 @@
 
 using namespace rive;
 
-Scene::Scene(ArtboardInstance* abi) : m_ArtboardInstance(abi)
+Scene::Scene(ArtboardInstance* abi) : m_artboardInstance(abi)
 {
-    assert(m_ArtboardInstance->isInstance());
+    assert(m_artboardInstance->isInstance());
 }
 
-float Scene::width() const { return m_ArtboardInstance->width(); }
+float Scene::width() const { return m_artboardInstance->width(); }
 
-float Scene::height() const { return m_ArtboardInstance->height(); }
+float Scene::height() const { return m_artboardInstance->height(); }
 
-void Scene::draw(Renderer* renderer) { m_ArtboardInstance->draw(renderer); }
+void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); }
 
 void Scene::pointerDown(Vec2D) {}
 void Scene::pointerMove(Vec2D) {}
diff --git a/test/assets/events_on_states.riv b/test/assets/events_on_states.riv
new file mode 100644
index 0000000..ea95f1c
--- /dev/null
+++ b/test/assets/events_on_states.riv
Binary files differ
diff --git a/test/state_machine_event_test.cpp b/test/state_machine_event_test.cpp
index 9ddab3d..a72e47d 100644
--- a/test/state_machine_event_test.cpp
+++ b/test/state_machine_event_test.cpp
@@ -16,6 +16,7 @@
 #include "rive/animation/blend_state_transition.hpp"
 #include "rive/animation/listener_input_change.hpp"
 #include "rive/animation/listener_fire_event.hpp"
+#include "rive/animation/entry_state.hpp"
 #include "rive/node.hpp"
 #include "catch.hpp"
 #include "rive_file_reader.hpp"
@@ -164,3 +165,53 @@
     stateMachineInstance->advance(0.0f);
     REQUIRE(stateMachineInstance->firedEventCount() == 0);
 }
+
+TEST_CASE("events load correctly on a state and transition", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/events_on_states.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->stateMachine()->layerCount() == 1);
+    auto layer = stateMachineInstance->stateMachine()->layer(0);
+    REQUIRE(layer->stateCount() == 5);
+    REQUIRE(layer->entryState()->transitionCount() == 1);
+    auto transition = layer->entryState()->transition(0);
+
+    // No events on transition from entry.
+    REQUIRE(transition->events().size() == 0);
+    REQUIRE(transition->stateTo()->is<rive::AnimationState>());
+    auto firstAnimationState = transition->stateTo()->as<rive::AnimationState>();
+    REQUIRE(firstAnimationState->events().size() == 2);
+    REQUIRE(firstAnimationState->transitionCount() == 1);
+    transition = firstAnimationState->transition(0);
+    // Transition from first animation state to next one should have two events.
+    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");
+
+    stateMachineInstance->advance(1.0f);
+    // Exits after 2 seconds so 1 second in no events should've fired yet
+    REQUIRE(stateMachineInstance->firedEventCount() == 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");
+
+    stateMachineInstance->advance(1.0f);
+    // Another second in the transition should complete
+    REQUIRE(stateMachineInstance->firedEventCount() == 1);
+    REQUIRE(stateMachineInstance->firedEventAt(0)->name() == "Fourth");
+}