Hit tests working from state machine
diff --git a/include/rive/animation/state_machine_input_instance.hpp b/include/rive/animation/state_machine_input_instance.hpp
index 06af9b9..8c5a786 100644
--- a/include/rive/animation/state_machine_input_instance.hpp
+++ b/include/rive/animation/state_machine_input_instance.hpp
@@ -74,6 +74,10 @@
 
     public:
         void fire();
+
+#ifdef TESTING
+        bool didFire() { return m_Fired; }
+#endif
     };
 } // 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 b995265..1c7ecf2 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -35,7 +35,8 @@
         void markNeedsAdvance();
 
         std::vector<HitShape*> m_HitShapes;
-        void processEvent(Vec2D position, EventType hitEvent);
+        /// Provide a hitEvent if you want to process a down or an up for the pointer position too.
+        void processEvent(Vec2D position, EventType hitEvent = EventType::updateHover);
 
     public:
         StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance);
@@ -71,6 +72,10 @@
         // previously called advance. If the index of out of range, it returns
         // the empty string.
         const LayerState* stateChangedByIndex(size_t index) const;
+
+        void pointerMove(const Vec2D& position);
+        void pointerDown(const Vec2D& position);
+        void pointerUp(const Vec2D& position);
     };
 } // namespace rive
 #endif
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index 297cedb..2266d57 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -67,6 +67,9 @@
         StatusCode initialize();
 
         Core* resolve(uint32_t id) const override;
+
+        /// Find the index of a component in the artboard the object in the artboard. The artboard
+        /// itself lives at index 0 so we use that as a flag for not found.
         uint32_t idOf(Core* object) const;
 
         // EXPERIMENTAL -- for internal testing only for now.
diff --git a/include/rive/event_type.hpp b/include/rive/event_type.hpp
index d50df00..deb1cea 100644
--- a/include/rive/event_type.hpp
+++ b/include/rive/event_type.hpp
@@ -1,7 +1,8 @@
 #ifndef _RIVE_EVENT_TYPE_HPP_
 #define _RIVE_EVENT_TYPE_HPP_
 namespace rive {
-    enum class EventType : unsigned int {
+    enum class EventType : int {
+        updateHover = -1,
         enter = 0,
         exit = 1,
         down = 2,
diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp
index f8933bb..a4e6f37 100644
--- a/include/rive/shapes/shape.hpp
+++ b/include/rive/shapes/shape.hpp
@@ -10,6 +10,7 @@
 namespace rive {
     class Path;
     class PathComposer;
+    class HitTester;
     class Shape : public ShapeBase, public ShapePaintContainer {
     private:
         PathComposer m_PathComposer;
@@ -28,6 +29,7 @@
         void update(ComponentDirt value) override;
         void draw(Renderer* renderer) override;
         Core* hitTest(HitInfo*, const Mat2D&) override;
+        bool hitTest(const IAABB& area) const;
 
         PathComposer* pathComposer() const { return (PathComposer*)&m_PathComposer; }
 
diff --git a/src/animation/state_machine_event.cpp b/src/animation/state_machine_event.cpp
index fd367ed..d6a3bc4 100644
--- a/src/animation/state_machine_event.cpp
+++ b/src/animation/state_machine_event.cpp
@@ -31,19 +31,24 @@
 
 StatusCode StateMachineEvent::onAddedClean(CoreContext* context) {
     auto artboard = static_cast<Artboard*>(context);
+    auto target = artboard->resolve(targetId());
+
     for (auto core : artboard->objects()) {
         if (core == nullptr) {
             continue;
         }
-        auto target = artboard->resolve(targetId());
 
         // Iterate artboard to find Shapes that are parented to the target
         if (core->is<Shape>()) {
             auto shape = core->as<Shape>();
+
             for (ContainerComponent* component = shape; component != nullptr;
                  component = component->parent()) {
                 if (component == target) {
-                    m_HitShapesIds.push_back(artboard->idOf(shape));
+                    auto index = artboard->idOf(shape);
+                    if (index != 0) {
+                        m_HitShapesIds.push_back(artboard->idOf(shape));
+                    }
                     break;
                 }
             }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index 1129c4f..5831ef2 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -16,6 +16,7 @@
 #include "rive/animation/state_machine_event.hpp"
 #include "rive/shapes/shape.hpp"
 #include "rive/math/aabb.hpp"
+#include "rive/math/hit_test.hpp"
 #include <unordered_map>
 
 using namespace rive;
@@ -241,15 +242,16 @@
                         position.x() + hitRadius,
                         position.y() + hitRadius)
                        .round();
+
     for (auto hitShape : m_HitShapes) {
 
         // TODO: quick reject.
 
-        // TODO: hit test
-        bool isOver = false;
+        bool isOver = hitShape->shape()->hitTest(hitArea);
 
         bool hoverChange = hitShape->isHovered != isOver;
         hitShape->isHovered = isOver;
+
         // iterate all events associated with this hit shape
         for (auto event : hitShape->events) {
             // Always update hover states regardless of which specific event type
@@ -271,10 +273,21 @@
     }
 }
 
+void StateMachineInstance::pointerMove(const Vec2D& position) {
+    processEvent(position, EventType::updateHover);
+}
+void StateMachineInstance::pointerDown(const Vec2D& position) {
+    processEvent(position, EventType::down);
+}
+void StateMachineInstance::pointerUp(const Vec2D& position) {
+    processEvent(position, EventType::up);
+}
+
 StateMachineInstance::StateMachineInstance(const StateMachine* machine,
                                            ArtboardInstance* instance) :
     m_Machine(machine), m_ArtboardInstance(instance) {
     assert(instance->isInstance());
+
     m_InputCount = machine->inputCount();
     m_InputInstances = new SMIInput*[m_InputCount];
     for (size_t i = 0; i < m_InputCount; i++) {
diff --git a/src/artboard.cpp b/src/artboard.cpp
index b52587f..f264595 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -288,7 +288,15 @@
     return m_Objects[id];
 }
 
-uint32_t Artboard::idOf(Core* object) const { return 0; }
+uint32_t Artboard::idOf(Core* object) const {
+    auto it = std::find(m_Objects.begin(), m_Objects.end(), object);
+
+    if (it != m_Objects.end()) {
+        return it - m_Objects.begin();
+    } else {
+        return 0;
+    }
+}
 
 void Artboard::onComponentDirty(Component* component) {
     m_Dirt |= ComponentDirt::Components;
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index b620f93..6851c73 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -57,6 +57,16 @@
     }
 }
 
+bool Shape::hitTest(const IAABB& area) const {
+    HitTestCommandPath tester(area);
+
+    for (auto path : m_Paths) {
+        tester.setXform(path->pathTransform());
+        path->buildPath(tester);
+    }
+    return tester.wasHit();
+}
+
 Core* Shape::hitTest(HitInfo* hinfo, const Mat2D& xform) {
     if (renderOpacity() == 0.0f) {
         return nullptr;
diff --git a/test/state_machine_event_test.cpp b/test/state_machine_event_test.cpp
index b65b32a..5dbdffa 100644
--- a/test/state_machine_event_test.cpp
+++ b/test/state_machine_event_test.cpp
@@ -59,3 +59,25 @@
     REQUIRE(inputChange3 != nullptr);
     REQUIRE(inputChange3->inputId() == 2);
 }
+
+TEST_CASE("hit testing via a state machine works", "[file]") {
+    RiveFileReader reader("../../test/assets/bullet_man.riv");
+
+    auto artboard = reader.file()->artboard("Bullet Man");
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachineInstance(0);
+    REQUIRE(stateMachine != nullptr);
+    // Advance artboard once so design time state is effectively in the transforms.
+    artboard->advance(0.0f);
+    stateMachine->advance(0.0f);
+    // Don't advance artboard again after applying state machine or our pointerDown will be off. The
+    // coordinates used in this test were from the design-time state.
+
+    auto trigger = stateMachine->getTrigger("Light");
+    REQUIRE(trigger != nullptr);
+    stateMachine->pointerDown(rive::Vec2D(71.0f, 263.0f));
+
+    REQUIRE(trigger->didFire());
+}