feat(focus): ui pass 1 (#13029) 72963a4d12
* core updates

* wip

* icons

* rebased

* rive remote and initial value fix

* forgot the riv...

* feat(editor): stage focus overlay indicating focus during playback

Add a corner-bracket overlay on the design stage that shows where focus
is while a state machine plays, without needing to wire focus state
through view model properties.

- Blue brackets around a directly focused element in the active artboard
- Grey brackets around a nested artboard/component when focus is
  contained within it (matched via shared focus-node identity)
- d-pad next/previous now wakes the stage so the bracket updates
  immediately instead of only on hover

* feat(focus): unify focus listener actions, add Clear focus, and focus transition conditions

Editor + core changes for focus authoring in state machines:

Listener actions
- Merge the separate "Focus Target" and "Focus Traversal" listener behaviours
  into a single "Focus" action with a leading mode combo (target / traverse /
  clear). Combos size to their widest option.
- Add a new FocusActionClear core type (Dart + C++ runtime + rml) that clears
  the artboard focus at runtime.

Viewport
- Move the "Focus Indicator" option above "Show Final Playback" via a decoupled
  ViewportOption.displayOrder (enum declaration order stays fixed for the
  persisted bitmask).

Transition conditions
- Add a "Focus" left-hand condition option (in the artboard section, behind the
  focusManagement flag): `focus == / != <focusable component>`, with a
  right-hand dropdown listing components that have a FocusData child.
- New TransitionFocusCondition core type (extends TransitionViewModelCondition),
  reusing a component comparator to store the target. Self-contained evaluation
  in Dart and C++ (queries FocusManager::hasPrimaryFocus); added a const
  focusManager() accessor on StateMachineInstance. Registered in rive_core,
  runtime, and rml core registries.

Note: focus_overlay.dart carries a small pre-existing change (gating the overlay
on the focusBrackets viewport option) included with this branch's focus work.

* review: address Copilot feedback on focus condition

- TransitionFocusCondition (Dart): resolve the target component comparator
  from either the right OR left slot, matching the C++ runtime. A .riv
  re-imported into the editor assigns the single exported comparator to the
  left slot, which previously made target/validateCondition/evaluate/uniqueId
  treat the condition as invalid and always evaluate false.
- state_machine_manager: focus condition create/select helpers now use the
  slot-agnostic targetComparator so editing an imported focus condition works.
- StageViewport: assert ViewportOption.displayOrder contains every option so a
  future enum addition can't silently drop a toggle from the menus.

* fix(editor): remove unused imports failing flutter analyze

CI runs `flutter analyze --no-fatal-infos --fatal-warnings`, which promoted two
unused_import warnings in focus_data_inspector_builder.dart (component.dart and
core_text_field.dart) to fatal errors. Neither symbol is referenced; removed
them so the editor analyze step passes.

* test(editor): allowlist condition_row.dart in theme rename audit

The new `_FocusConditionFlow` paints `colors.warning` (0xFFF6C566) on its node
connectors in the invalid state, mirroring `_DataPropertyFlow` — a legitimate
+1 warning-hex site vs mainline, not a rename miswire. Add condition_row.dart
to the audit allowlist with a justification (baseline regeneration only applies
to changes already on mainline).

* test(editor): allowlist backboard_inspector_builder.dart in theme rename audit

The focus feature removed the legacy _FocusButton / _DPad / _DPadButton widgets
from the backboard inspector (superseded by per-component focus authoring).
Those widgets held all of the file's RiveColors sites (border, trackEmphasis,
textSecondary x2 each), so the branch paints 0 vs mainline's 6 — an intentional
removal, not a rename. Add it to the audit allowlist with a justification.

* chore(rive_core): regenerate core from defs (focus types)

* fix(core): reassign focus type keys to avoid collision with master

Master's data-bind font PR (#13031) added ViewModelPropertyAssetFont (1034)
and ViewModelInstanceAssetFont (1035), which now collide with the focus types
introduced here. Reassign FocusActionClear -> 1037 and TransitionFocusCondition
-> 1038 (next free keys after master's 1036) in the defs and generated base
files (Dart + runtime + rml). Registrations reference the typeKey constants, so
no other changes are needed.

* style(runtime,rml): clang-format focus C++ files

* test(runtime): add C++ tests for FocusActionClear and TransitionFocusCondition

* test(runtime): avoid ODR-use of TransitionFocusCondition typeKey static

* runtime test

* feedback

Co-authored-by: Alex Gibson <agibson.uk@gmail.com>
diff --git a/.rive_head b/.rive_head
index 786a329..69cffd2 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-b20256f36800e04af741647860cb9013d148c76e
+72963a4d12749be5f85af405b9cb903d8a82504c
diff --git a/dev/defs/animation/focus_action_clear.json b/dev/defs/animation/focus_action_clear.json
new file mode 100644
index 0000000..10ea749
--- /dev/null
+++ b/dev/defs/animation/focus_action_clear.json
@@ -0,0 +1,8 @@
+{
+  "name": "FocusActionClear",
+  "key": {
+    "int": 1037,
+    "string": "focusactionclear"
+  },
+  "extends": "animation/focus_action.json"
+}
diff --git a/dev/defs/animation/transition_focus_condition.json b/dev/defs/animation/transition_focus_condition.json
new file mode 100644
index 0000000..c3e7519
--- /dev/null
+++ b/dev/defs/animation/transition_focus_condition.json
@@ -0,0 +1,8 @@
+{
+  "name": "TransitionFocusCondition",
+  "key": {
+    "int": 1038,
+    "string": "transitionfocuscondition"
+  },
+  "extends": "animation/transition_viewmodel_condition.json"
+}
diff --git a/dev/defs/focus_data.json b/dev/defs/focus_data.json
index 817271f..d0b84e9 100644
--- a/dev/defs/focus_data.json
+++ b/dev/defs/focus_data.json
@@ -6,32 +6,52 @@
   },
   "extends": "component.json",
   "properties": {
+    "focusFlags": {
+      "type": "uint",
+      "initialValue": "7",
+      "animates": true,
+      "key": {
+        "int": 1033,
+        "string": "focusflags"
+      },
+      "description": "Bitmask for focus flags",
+      "bindable": true
+    },
     "canFocus": {
       "type": "bool",
-      "initialValue": "true",
+      "animates": true,
       "key": {
         "int": 953,
         "string": "canfocus"
       },
-      "description": "Master switch - can this node receive focus at all?"
+      "description": "Master switch - can this node receive focus at all?",
+      "bindable": true,
+      "passthroughForBitmask": "focusFlags",
+      "passthroughBit": 0
     },
     "canTouch": {
       "type": "bool",
-      "initialValue": "true",
+      "animates": true,
       "key": {
         "int": 954,
         "string": "cantouch"
       },
-      "description": "Can receive focus via pointer/touch click"
+      "description": "Can receive focus via pointer/touch click",
+      "bindable": true,
+      "passthroughForBitmask": "focusFlags",
+      "passthroughBit": 1
     },
     "canTraverse": {
       "type": "bool",
-      "initialValue": "true",
+      "animates": true,
       "key": {
         "int": 955,
         "string": "cantraverse"
       },
-      "description": "Included in Tab/Shift+Tab traversal"
+      "description": "Included in Tab/Shift+Tab traversal",
+      "bindable": true,
+      "passthroughForBitmask": "focusFlags",
+      "passthroughBit": 2
     },
     "edgeBehaviorValue": {
       "type": "uint",
diff --git a/include/rive/animation/focus_action_clear.hpp b/include/rive/animation/focus_action_clear.hpp
new file mode 100644
index 0000000..a17d954
--- /dev/null
+++ b/include/rive/animation/focus_action_clear.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_FOCUS_ACTION_CLEAR_HPP_
+#define _RIVE_FOCUS_ACTION_CLEAR_HPP_
+#include "rive/generated/animation/focus_action_clear_base.hpp"
+namespace rive
+{
+class FocusActionClear : public FocusActionClearBase
+{
+public:
+    void perform(StateMachineInstance* stateMachineInstance,
+                 const ListenerInvocation& invocation) const override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index a45b556..24774a8 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -249,6 +249,14 @@
                                       : &m_focusManager;
     }
 
+    /// Const overload of [focusManager], used by read-only consumers such as
+    /// condition evaluation.
+    const FocusManager* focusManager() const
+    {
+        return m_externalFocusManager ? m_externalFocusManager
+                                      : &m_focusManager;
+    }
+
     /// Check if this state machine is using an external focus manager.
     bool hasExternalFocusManager() const
     {
diff --git a/include/rive/animation/transition_focus_condition.hpp b/include/rive/animation/transition_focus_condition.hpp
new file mode 100644
index 0000000..7e7eaa5
--- /dev/null
+++ b/include/rive/animation/transition_focus_condition.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_TRANSITION_FOCUS_CONDITION_HPP_
+#define _RIVE_TRANSITION_FOCUS_CONDITION_HPP_
+#include "rive/generated/animation/transition_focus_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+// Condition that evaluates whether a specific focusable component currently
+// holds focus. The target component is stored on the right comparator (a
+// TransitionPropertyComponentComparator pointing at the focusable node); the
+// left side is the implicit "current focus". Only ==/!= are meaningful.
+class TransitionFocusCondition : public TransitionFocusConditionBase
+{
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance,
+                  StateMachineLayerInstance* layerInstance) const override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/focus_data.hpp b/include/rive/focus_data.hpp
index 10b21f1..87dfbf1 100644
--- a/include/rive/focus_data.hpp
+++ b/include/rive/focus_data.hpp
@@ -87,9 +87,7 @@
     void update(ComponentDirt value) override;
 
 protected:
-    void canFocusChanged() override;
-    void canTouchChanged() override;
-    void canTraverseChanged() override;
+    void focusFlagsChanged() override;
     void edgeBehaviorValueChanged() override;
     void nameChanged() override;
 
diff --git a/include/rive/generated/animation/focus_action_clear_base.hpp b/include/rive/generated/animation/focus_action_clear_base.hpp
new file mode 100644
index 0000000..1b67298
--- /dev/null
+++ b/include/rive/generated/animation/focus_action_clear_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_FOCUS_ACTION_CLEAR_BASE_HPP_
+#define _RIVE_FOCUS_ACTION_CLEAR_BASE_HPP_
+#include "rive/animation/focus_action.hpp"
+namespace rive
+{
+class FocusActionClearBase : public FocusAction
+{
+protected:
+    typedef FocusAction Super;
+
+public:
+    static const uint16_t typeKey = 1037;
+
+    /// Helper to quickly determine if a core object extends another without
+    /// RTTI at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FocusActionClearBase::typeKey:
+            case FocusActionBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+    void copy(const FocusActionClearBase& object) { FocusAction::copy(object); }
+
+protected:
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/generated/animation/transition_focus_condition_base.hpp b/include/rive/generated/animation/transition_focus_condition_base.hpp
new file mode 100644
index 0000000..a9759a0
--- /dev/null
+++ b/include/rive/generated/animation/transition_focus_condition_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_FOCUS_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_FOCUS_CONDITION_BASE_HPP_
+#include "rive/animation/transition_viewmodel_condition.hpp"
+namespace rive
+{
+class TransitionFocusConditionBase : public TransitionViewModelCondition
+{
+protected:
+    typedef TransitionViewModelCondition Super;
+
+public:
+    static const uint16_t typeKey = 1038;
+
+    /// Helper to quickly determine if a core object extends another without
+    /// RTTI at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionFocusConditionBase::typeKey:
+            case TransitionViewModelConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 36e1fc9..adf504c 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -21,6 +21,7 @@
 #include "rive/animation/entry_state.hpp"
 #include "rive/animation/exit_state.hpp"
 #include "rive/animation/focus_action.hpp"
+#include "rive/animation/focus_action_clear.hpp"
 #include "rive/animation/focus_action_target.hpp"
 #include "rive/animation/focus_action_traversal.hpp"
 #include "rive/animation/interpolating_keyframe.hpp"
@@ -78,6 +79,7 @@
 #include "rive/animation/state_transition.hpp"
 #include "rive/animation/transition_artboard_condition.hpp"
 #include "rive/animation/transition_bool_condition.hpp"
+#include "rive/animation/transition_focus_condition.hpp"
 #include "rive/animation/transition_comparator.hpp"
 #include "rive/animation/transition_condition.hpp"
 #include "rive/animation/transition_input_condition.hpp"
@@ -555,6 +557,8 @@
                 return new TransitionViewModelCondition();
             case TransitionArtboardConditionBase::typeKey:
                 return new TransitionArtboardCondition();
+            case TransitionFocusConditionBase::typeKey:
+                return new TransitionFocusCondition();
             case AnyStateBase::typeKey:
                 return new AnyState();
             case BlendState1DInputBase::typeKey:
@@ -583,6 +587,8 @@
                 return new KeyFrameColor();
             case FocusActionTraversalBase::typeKey:
                 return new FocusActionTraversal();
+            case FocusActionClearBase::typeKey:
+                return new FocusActionClear();
             case StateMachineBase::typeKey:
                 return new StateMachine();
             case StateMachineFireEventBase::typeKey:
@@ -1458,6 +1464,9 @@
             case ImageBase::fitPropertyKey:
                 object->as<ImageBase>()->fit(value);
                 break;
+            case FocusDataBase::focusFlagsPropertyKey:
+                object->as<FocusDataBase>()->focusFlags(value);
+                break;
             case FocusDataBase::edgeBehaviorValuePropertyKey:
                 object->as<FocusDataBase>()->edgeBehaviorValue(value);
                 break;
@@ -1977,14 +1986,56 @@
                 object->as<ClippingShapeBase>()->isVisible(value);
                 break;
             case FocusDataBase::canFocusPropertyKey:
-                object->as<FocusDataBase>()->canFocus(value);
+            {
+                auto* _o = object->as<FocusDataBase>();
+                if (_o)
+                {
+                    const uint32_t _cur = _o->focusFlags();
+                    const uint32_t _bm = static_cast<uint32_t>(1u << 0);
+                    const uint32_t _next = static_cast<uint32_t>(
+                        (_cur & ~_bm) |
+                        (value ? _bm : static_cast<uint32_t>(0)));
+                    if (_cur != _next)
+                    {
+                        _o->focusFlags(_next);
+                    }
+                }
                 break;
+            }
             case FocusDataBase::canTouchPropertyKey:
-                object->as<FocusDataBase>()->canTouch(value);
+            {
+                auto* _o = object->as<FocusDataBase>();
+                if (_o)
+                {
+                    const uint32_t _cur = _o->focusFlags();
+                    const uint32_t _bm = static_cast<uint32_t>(1u << 1);
+                    const uint32_t _next = static_cast<uint32_t>(
+                        (_cur & ~_bm) |
+                        (value ? _bm : static_cast<uint32_t>(0)));
+                    if (_cur != _next)
+                    {
+                        _o->focusFlags(_next);
+                    }
+                }
                 break;
+            }
             case FocusDataBase::canTraversePropertyKey:
-                object->as<FocusDataBase>()->canTraverse(value);
+            {
+                auto* _o = object->as<FocusDataBase>();
+                if (_o)
+                {
+                    const uint32_t _cur = _o->focusFlags();
+                    const uint32_t _bm = static_cast<uint32_t>(1u << 2);
+                    const uint32_t _next = static_cast<uint32_t>(
+                        (_cur & ~_bm) |
+                        (value ? _bm : static_cast<uint32_t>(0)));
+                    if (_cur != _next)
+                    {
+                        _o->focusFlags(_next);
+                    }
+                }
                 break;
+            }
             case CustomPropertyBooleanBase::propertyValuePropertyKey:
                 object->as<CustomPropertyBooleanBase>()->propertyValue(value);
                 break;
@@ -3477,6 +3528,8 @@
                 return object->as<ImageBase>()->assetId();
             case ImageBase::fitPropertyKey:
                 return object->as<ImageBase>()->fit();
+            case FocusDataBase::focusFlagsPropertyKey:
+                return object->as<FocusDataBase>()->focusFlags();
             case FocusDataBase::edgeBehaviorValuePropertyKey:
                 return object->as<FocusDataBase>()->edgeBehaviorValue();
             case DrawRulesBase::drawTargetIdPropertyKey:
@@ -3830,12 +3883,6 @@
                 return object->as<RectangleBase>()->linkCornerRadius();
             case ClippingShapeBase::isVisiblePropertyKey:
                 return object->as<ClippingShapeBase>()->isVisible();
-            case FocusDataBase::canFocusPropertyKey:
-                return object->as<FocusDataBase>()->canFocus();
-            case FocusDataBase::canTouchPropertyKey:
-                return object->as<FocusDataBase>()->canTouch();
-            case FocusDataBase::canTraversePropertyKey:
-                return object->as<FocusDataBase>()->canTraverse();
             case CustomPropertyBooleanBase::propertyValuePropertyKey:
                 return object->as<CustomPropertyBooleanBase>()->propertyValue();
             case LayoutComponentBase::clipPropertyKey:
@@ -4501,6 +4548,7 @@
             case PolygonBase::pointsPropertyKey:
             case ImageBase::assetIdPropertyKey:
             case ImageBase::fitPropertyKey:
+            case FocusDataBase::focusFlagsPropertyKey:
             case FocusDataBase::edgeBehaviorValuePropertyKey:
             case DrawRulesBase::drawTargetIdPropertyKey:
             case LayoutComponentBase::styleIdPropertyKey:
@@ -4657,9 +4705,6 @@
             case PointsCommonPathBase::isClosedPropertyKey:
             case RectangleBase::linkCornerRadiusPropertyKey:
             case ClippingShapeBase::isVisiblePropertyKey:
-            case FocusDataBase::canFocusPropertyKey:
-            case FocusDataBase::canTouchPropertyKey:
-            case FocusDataBase::canTraversePropertyKey:
             case CustomPropertyBooleanBase::propertyValuePropertyKey:
             case LayoutComponentBase::clipPropertyKey:
             case DataBindPathBase::isRelativePropertyKey:
@@ -5259,6 +5304,8 @@
                 return object->is<ImageBase>();
             case ImageBase::fitPropertyKey:
                 return object->is<ImageBase>();
+            case FocusDataBase::focusFlagsPropertyKey:
+                return object->is<FocusDataBase>();
             case FocusDataBase::edgeBehaviorValuePropertyKey:
                 return object->is<FocusDataBase>();
             case DrawRulesBase::drawTargetIdPropertyKey:
diff --git a/include/rive/generated/focus_data_base.hpp b/include/rive/generated/focus_data_base.hpp
index e680398..25ea166 100644
--- a/include/rive/generated/focus_data_base.hpp
+++ b/include/rive/generated/focus_data_base.hpp
@@ -29,52 +29,30 @@
 
     uint16_t coreType() const override { return typeKey; }
 
+    static const uint16_t focusFlagsPropertyKey = 1033;
     static const uint16_t canFocusPropertyKey = 953;
+    static const uint32_t canFocusBitmask = 1u << 0;
     static const uint16_t canTouchPropertyKey = 954;
+    static const uint32_t canTouchBitmask = 1u << 1;
     static const uint16_t canTraversePropertyKey = 955;
+    static const uint32_t canTraverseBitmask = 1u << 2;
     static const uint16_t edgeBehaviorValuePropertyKey = 956;
 
 protected:
-    bool m_CanFocus = true;
-    bool m_CanTouch = true;
-    bool m_CanTraverse = true;
+    uint32_t m_FocusFlags = 7;
     uint32_t m_EdgeBehaviorValue = 0;
 
 public:
-    inline bool canFocus() const { return m_CanFocus; }
-    void canFocus(bool value)
+    inline uint32_t focusFlags() const { return m_FocusFlags; }
+    void focusFlags(uint32_t value)
     {
-        if (m_CanFocus == value)
+        if (m_FocusFlags == value)
         {
             return;
         }
-        m_CanFocus = value;
-        canFocusChanged();
-        notifyPropertyChanged(canFocusPropertyKey);
-    }
-
-    inline bool canTouch() const { return m_CanTouch; }
-    void canTouch(bool value)
-    {
-        if (m_CanTouch == value)
-        {
-            return;
-        }
-        m_CanTouch = value;
-        canTouchChanged();
-        notifyPropertyChanged(canTouchPropertyKey);
-    }
-
-    inline bool canTraverse() const { return m_CanTraverse; }
-    void canTraverse(bool value)
-    {
-        if (m_CanTraverse == value)
-        {
-            return;
-        }
-        m_CanTraverse = value;
-        canTraverseChanged();
-        notifyPropertyChanged(canTraversePropertyKey);
+        m_FocusFlags = value;
+        focusFlagsChanged();
+        notifyPropertyChanged(focusFlagsPropertyKey);
     }
 
     inline uint32_t edgeBehaviorValue() const { return m_EdgeBehaviorValue; }
@@ -92,9 +70,7 @@
     Core* clone() const override;
     void copy(const FocusDataBase& object)
     {
-        m_CanFocus = object.m_CanFocus;
-        m_CanTouch = object.m_CanTouch;
-        m_CanTraverse = object.m_CanTraverse;
+        m_FocusFlags = object.m_FocusFlags;
         m_EdgeBehaviorValue = object.m_EdgeBehaviorValue;
         Component::copy(object);
     }
@@ -103,14 +79,8 @@
     {
         switch (propertyKey)
         {
-            case canFocusPropertyKey:
-                m_CanFocus = CoreBoolType::deserialize(reader);
-                return true;
-            case canTouchPropertyKey:
-                m_CanTouch = CoreBoolType::deserialize(reader);
-                return true;
-            case canTraversePropertyKey:
-                m_CanTraverse = CoreBoolType::deserialize(reader);
+            case focusFlagsPropertyKey:
+                m_FocusFlags = CoreUintType::deserialize(reader);
                 return true;
             case edgeBehaviorValuePropertyKey:
                 m_EdgeBehaviorValue = CoreUintType::deserialize(reader);
@@ -120,9 +90,7 @@
     }
 
 protected:
-    virtual void canFocusChanged() {}
-    virtual void canTouchChanged() {}
-    virtual void canTraverseChanged() {}
+    virtual void focusFlagsChanged() {}
     virtual void edgeBehaviorValueChanged() {}
 };
 } // namespace rive
diff --git a/src/animation/focus_action_clear.cpp b/src/animation/focus_action_clear.cpp
new file mode 100644
index 0000000..c5d2ebb
--- /dev/null
+++ b/src/animation/focus_action_clear.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 Rive
+ */
+
+#include "rive/animation/focus_action_clear.hpp"
+#include "rive/animation/listener_invocation.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/input/focus_manager.hpp"
+
+using namespace rive;
+
+void FocusActionClear::perform(StateMachineInstance* stateMachineInstance,
+                               const ListenerInvocation& invocation) const
+{
+    (void)invocation;
+    if (stateMachineInstance == nullptr)
+    {
+        return;
+    }
+
+    FocusManager* manager = stateMachineInstance->focusManager();
+    if (manager != nullptr)
+    {
+        manager->clearFocus();
+    }
+}
diff --git a/src/animation/transition_focus_condition.cpp b/src/animation/transition_focus_condition.cpp
new file mode 100644
index 0000000..7e19f3e
--- /dev/null
+++ b/src/animation/transition_focus_condition.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 Rive
+ */
+
+#include "rive/animation/transition_focus_condition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+#include "rive/animation/transition_property_component_comparator.hpp"
+#include "rive/artboard.hpp"
+#include "rive/focus_data.hpp"
+#include "rive/input/focus_manager.hpp"
+#include "rive/node.hpp"
+
+using namespace rive;
+
+bool TransitionFocusCondition::evaluate(
+    const StateMachineInstance* stateMachineInstance,
+    StateMachineLayerInstance* layerInstance) const
+{
+    (void)layerInstance;
+    if (stateMachineInstance == nullptr)
+    {
+        return false;
+    }
+
+    // The target focusable component is stored on a component comparator. In
+    // the editor it lives on the right (the left side is the implicit "current
+    // focus"), but on runtime import the single exported comparator is assigned
+    // to the left slot, so accept it from either side.
+    TransitionComparator* comparator = rightComparator();
+    if (comparator == nullptr ||
+        !comparator->is<TransitionPropertyComponentComparator>())
+    {
+        comparator = leftComparator();
+    }
+    if (comparator == nullptr ||
+        !comparator->is<TransitionPropertyComponentComparator>())
+    {
+        return false;
+    }
+    auto focusComparator =
+        comparator->as<TransitionPropertyComponentComparator>();
+
+    const FocusManager* manager = stateMachineInstance->focusManager();
+    if (manager == nullptr)
+    {
+        return false;
+    }
+
+    auto target =
+        stateMachineInstance->artboard()->resolve(focusComparator->objectId());
+
+    bool focused = false;
+    if (target != nullptr && target->is<Node>())
+    {
+        auto node = target->as<Node>();
+        for (auto child : node->children())
+        {
+            if (child->is<FocusData>())
+            {
+                auto focusNode = child->as<FocusData>()->focusNode();
+                // Use hasFocus (not hasPrimaryFocus): the target is considered
+                // focused when it or any descendant is the primary focus, so a
+                // scope/container counts as focused while a child leaf holds
+                // focus.
+                focused = focusNode != nullptr && manager->hasFocus(focusNode);
+                break;
+            }
+        }
+    }
+
+    return op() == TransitionConditionOp::equal ? focused : !focused;
+}
diff --git a/src/file.cpp b/src/file.cpp
index 3d66d88..9bb1150 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -546,6 +546,7 @@
                 break;
             case TransitionViewModelCondition::typeKey:
             case TransitionArtboardCondition::typeKey:
+            case TransitionFocusCondition::typeKey:
                 stackObject =
                     std::make_unique<TransitionViewModelConditionImporter>(
                         object->as<TransitionViewModelCondition>());
diff --git a/src/focus_data.cpp b/src/focus_data.cpp
index 631e619..d4a3387 100644
--- a/src/focus_data.cpp
+++ b/src/focus_data.cpp
@@ -57,9 +57,9 @@
     if (m_focusNode == nullptr)
     {
         m_focusNode = rcp<FocusNode>(new FocusNode(this));
-        m_focusNode->canFocus(m_CanFocus);
-        m_focusNode->canTouch(m_CanTouch);
-        m_focusNode->canTraverse(m_CanTraverse);
+        m_focusNode->canFocus((focusFlags() & canFocusBitmask) != 0);
+        m_focusNode->canTouch((focusFlags() & canTouchBitmask) != 0);
+        m_focusNode->canTraverse((focusFlags() & canTraverseBitmask) != 0);
         m_focusNode->edgeBehavior(
             static_cast<EdgeBehavior>(m_EdgeBehaviorValue));
         m_focusNode->name(name());
@@ -369,27 +369,13 @@
     }
 }
 
-void FocusData::canFocusChanged()
+void FocusData::focusFlagsChanged()
 {
     if (m_focusNode != nullptr)
     {
-        m_focusNode->canFocus(m_CanFocus);
-    }
-}
-
-void FocusData::canTouchChanged()
-{
-    if (m_focusNode != nullptr)
-    {
-        m_focusNode->canTouch(m_CanTouch);
-    }
-}
-
-void FocusData::canTraverseChanged()
-{
-    if (m_focusNode != nullptr)
-    {
-        m_focusNode->canTraverse(m_CanTraverse);
+        m_focusNode->canFocus((focusFlags() & canFocusBitmask) != 0);
+        m_focusNode->canTouch((focusFlags() & canTouchBitmask) != 0);
+        m_focusNode->canTraverse((focusFlags() & canTraverseBitmask) != 0);
     }
 }
 
diff --git a/src/generated/animation/focus_action_clear_base.cpp b/src/generated/animation/focus_action_clear_base.cpp
new file mode 100644
index 0000000..adec5af
--- /dev/null
+++ b/src/generated/animation/focus_action_clear_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/focus_action_clear_base.hpp"
+#include "rive/animation/focus_action_clear.hpp"
+
+using namespace rive;
+
+Core* FocusActionClearBase::clone() const
+{
+    auto cloned = new FocusActionClear();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_focus_condition_base.cpp b/src/generated/animation/transition_focus_condition_base.cpp
new file mode 100644
index 0000000..6ff444a
--- /dev/null
+++ b/src/generated/animation/transition_focus_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_focus_condition_base.hpp"
+#include "rive/animation/transition_focus_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionFocusConditionBase::clone() const
+{
+    auto cloned = new TransitionFocusCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/tests/unit_tests/assets/focus_test.riv b/tests/unit_tests/assets/focus_test.riv
new file mode 100644
index 0000000..a328f39
--- /dev/null
+++ b/tests/unit_tests/assets/focus_test.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/focus_test.cpp b/tests/unit_tests/runtime/focus_test.cpp
index e688758..0e856b6 100644
--- a/tests/unit_tests/runtime/focus_test.cpp
+++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -1,5 +1,9 @@
 #include <catch.hpp>
+#include "rive/animation/focus_action_clear.hpp"
 #include "rive/animation/focus_action_traversal.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+#include "rive/animation/transition_focus_condition.hpp"
+#include "rive/animation/transition_property_component_comparator.hpp"
 #include "rive/animation/listener_invocation.hpp"
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_machine_instance.hpp"
@@ -1089,6 +1093,104 @@
     CHECK(manager.primaryFocus() == leaf2);
 }
 
+// =============================================================================
+// FocusActionClear Tests
+// =============================================================================
+
+TEST_CASE("FocusActionClear perform clears the primary focus",
+          "[FocusActionClear]")
+{
+    NoOpFactory factory;
+    Artboard artboard(&factory);
+    auto instance = artboard.instance();
+    StateMachine machine;
+    StateMachineInstance smi(&machine, instance.get());
+
+    FocusManager* fm = smi.focusManager();
+    MockFocusable f1;
+    auto node1 = make_rcp<FocusNode>(&f1);
+    fm->addChild(nullptr, node1);
+    fm->setFocus(node1);
+    REQUIRE(fm->primaryFocus() == node1);
+
+    FocusActionClear action;
+    action.perform(&smi, ListenerInvocation::none());
+
+    CHECK(fm->primaryFocus() == nullptr);
+}
+
+TEST_CASE("FocusActionClear perform is a no-op when nothing is focused",
+          "[FocusActionClear]")
+{
+    NoOpFactory factory;
+    Artboard artboard(&factory);
+    auto instance = artboard.instance();
+    StateMachine machine;
+    StateMachineInstance smi(&machine, instance.get());
+
+    REQUIRE(smi.focusManager()->primaryFocus() == nullptr);
+
+    FocusActionClear action;
+    action.perform(&smi, ListenerInvocation::none());
+
+    CHECK(smi.focusManager()->primaryFocus() == nullptr);
+}
+
+TEST_CASE("FocusActionClear perform ignores null StateMachineInstance",
+          "[FocusActionClear]")
+{
+    FocusActionClear action;
+    // Must not dereference the null instance.
+    action.perform(nullptr, ListenerInvocation::none());
+}
+
+// =============================================================================
+// TransitionFocusCondition Tests
+// =============================================================================
+
+TEST_CASE("TransitionFocusCondition uses the reassigned core type key",
+          "[TransitionFocusCondition]")
+{
+    // Locks in the collision fix: master's font PR claimed 1035, so this
+    // condition was reassigned to 1038. A regression here means a type-key
+    // clash on import/export.
+    // Copy into a local to avoid ODR-using the in-class static constant
+    // (which has no out-of-line definition) when binding it to Catch2's
+    // by-reference comparison expressions.
+    uint16_t typeKey = TransitionFocusConditionBase::typeKey;
+    CHECK(typeKey == 1038);
+
+    auto condition = std::make_unique<TransitionFocusCondition>();
+    CHECK(condition->coreType() == typeKey);
+    CHECK(condition->is<TransitionFocusCondition>());
+}
+
+TEST_CASE("TransitionFocusCondition evaluate returns false for a null "
+          "StateMachineInstance",
+          "[TransitionFocusCondition]")
+{
+    // Heap allocation value-initializes the (comparator) members to null, so
+    // the guard clauses and destructor are well-defined even without import.
+    auto condition = std::make_unique<TransitionFocusCondition>();
+    CHECK(condition->evaluate(nullptr, nullptr) == false);
+}
+
+TEST_CASE("TransitionFocusCondition evaluate returns false when no target "
+          "comparator is configured",
+          "[TransitionFocusCondition]")
+{
+    NoOpFactory factory;
+    Artboard artboard(&factory);
+    auto instance = artboard.instance();
+    StateMachine machine;
+    StateMachineInstance smi(&machine, instance.get());
+
+    auto condition = std::make_unique<TransitionFocusCondition>();
+    // With neither comparator set to a TransitionPropertyComponentComparator,
+    // there is no focus target to evaluate against, so the condition is false.
+    CHECK(condition->evaluate(&smi, nullptr) == false);
+}
+
 } // namespace rive
 
 TEST_CASE("FocusManager skips collapsed nodes and fully transparent nodes",
@@ -1723,4 +1825,39 @@
     artboard->draw(renderer.get());
 
     CHECK(silver.matches("list_focus_order"));
-}
\ No newline at end of file
+}
+
+TEST_CASE("Focus based transitions work", "[silver]")
+{
+    rive::SerializingFactory silver;
+    auto file = ReadRiveFile("assets/focus_test.riv", &silver);
+
+    auto artboard = file->artboardDefault();
+    REQUIRE(artboard != nullptr);
+
+    silver.frameSize(artboard->width(), artboard->height());
+
+    auto stateMachine = artboard->stateMachineAt(0);
+    auto focusManager = stateMachine->focusManager();
+
+    auto vmi = file->createDefaultViewModelInstance(artboard.get());
+
+    auto renderer = silver.makeRenderer();
+    stateMachine->bindViewModelInstance(vmi);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    stateMachine->pointerDown(rive::Vec2D(55.0, 65.0));
+    stateMachine->pointerUp(rive::Vec2D(55.0, 65.0));
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    stateMachine->pointerDown(rive::Vec2D(442.0, 65.0));
+    stateMachine->pointerUp(rive::Vec2D(442.0, 65.0));
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    CHECK(silver.matches("focus_test"));
+}
diff --git a/tests/unit_tests/silvers/focus_test.sriv b/tests/unit_tests/silvers/focus_test.sriv
new file mode 100644
index 0000000..79cc14b
--- /dev/null
+++ b/tests/unit_tests/silvers/focus_test.sriv
Binary files differ