Nnnnn focus management fixes (#12303) b2438309fa
* sort focus nodes hierarchically

* chore(focus): correctly handle focus with lists

* fix virtualizableChanged lifecycle

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index f2ece2f..aeb8937 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-fcbebb25e41ff16477bb48325bd5cd66d7f28038
+b2438309fa61b3d52d4d1a0e6e97f3ef5ce71bd6
diff --git a/include/rive/artboard_component_list.hpp b/include/rive/artboard_component_list.hpp
index e676e4e..98da956 100644
--- a/include/rive/artboard_component_list.hpp
+++ b/include/rive/artboard_component_list.hpp
@@ -10,11 +10,13 @@
 #include "rive/property_recorder.hpp"
 #include "rive/file.hpp"
 #include "rive/artboard_host.hpp"
+#include "rive/input/focus_node.hpp"
 #include "rive/data_bind/data_bind_list_item_consumer.hpp"
 #include "rive/layout/layout_node_provider.hpp"
 #include "rive/viewmodel/viewmodel_instance_list_item.hpp"
 #include "rive/viewmodel/symbol_type.hpp"
 #include "rive/virtualizing_component.hpp"
+#include "rive/refcnt.hpp"
 #include <memory>
 #include <stdio.h>
 #include <unordered_map>
@@ -25,6 +27,7 @@
 class ScrollConstraint;
 class ArtboardListMapRule;
 class ArtboardListDrawIndexDependent;
+class FocusManager;
 
 class ArtboardComponentList : public ArtboardComponentListBase,
                               public ArtboardHost,
@@ -106,6 +109,7 @@
     Artboard* findArtboard(
         const rcp<ViewModelInstanceListItem>& listItem) const;
     void addVirtualizable(int index) override;
+    void virtualizableChanged() override;
     void removeVirtualizable(int index) override;
     void setVisibleIndices(int start, int end) override
     {
@@ -137,6 +141,13 @@
     void addMapRule(ArtboardListMapRule*);
     int type() const override { return coreType(); }
 
+    /// Create/parent a synthetic list scope FocusNode (structural, no
+    /// Focusable) so list item focus trees group under it. Idempotent.
+    void ensureListScopeFocusNode(FocusManager* focusManager,
+                                  rcp<FocusNode> hostParent);
+    rcp<FocusNode> listScopeFocusNode() const { return m_listScopeFocusNode; }
+    void removeListScopeFocusNode();
+
     /// Rebuilds the ordered-list cache when invalid (list, visibility, or
     /// drawIndex sort inputs changed).
     void ensureOrderedListIndices();
@@ -196,6 +207,22 @@
         m_artboardOverridesMap;
     std::unordered_map<int, int> m_artboardMapRules;
 
+    // Synthetic scope that parents all list item focus subtrees; no Focusable.
+    rcp<FocusNode> m_listScopeFocusNode = nullptr;
+    // One structural row per list item index, direct child of
+    // m_listScopeFocusNode.
+    std::vector<rcp<FocusNode>> m_listRowFocusNodes;
+
+    void syncListRowNodesWithList(FocusManager* fm);
+    void syncListRowNodesWithList(
+        FocusManager* fm,
+        const std::vector<rcp<ViewModelInstanceListItem>>& previousListItems,
+        const std::vector<rcp<FocusNode>>& previousRowNodes);
+    rcp<FocusNode> makeListRowFocusNode() const;
+    void reparentListRowsInScope(FocusManager* fm);
+    bool listItemNeedsBuildUnderRow(FocusManager* parentFM,
+                                    ArtboardInstance* inst,
+                                    rcp<FocusNode> row) const;
     void attachArtboardOverride(ArtboardInstance*,
                                 rcp<ViewModelInstanceListItem>);
     void clearArtboardOverride(ArtboardInstance*);
diff --git a/include/rive/input/focus_manager.hpp b/include/rive/input/focus_manager.hpp
index da57b2d..41f9962 100644
--- a/include/rive/input/focus_manager.hpp
+++ b/include/rive/input/focus_manager.hpp
@@ -81,6 +81,9 @@
 
     // Add child to parent (or to root nodes if parent is null)
     void addChild(rcp<FocusNode> parent, rcp<FocusNode> child);
+    // Insert as index-th child of parent (0 = first). Same re-parenting as
+    // addChild.
+    void addChild(rcp<FocusNode> parent, rcp<FocusNode> child, size_t index);
 
     // Remove child from its current parent (clears focus if needed)
     void removeChild(rcp<FocusNode> child);
diff --git a/include/rive/input/focus_node.hpp b/include/rive/input/focus_node.hpp
index 51c8885..3b771c3 100644
--- a/include/rive/input/focus_node.hpp
+++ b/include/rive/input/focus_node.hpp
@@ -105,6 +105,7 @@
     bool isScope() const { return !m_children.empty(); }
 
     void addChild(rcp<FocusNode> child);
+    void insertChild(size_t index, rcp<FocusNode> child);
     void removeChild(rcp<FocusNode> child);
 
     // Remove this node from its current parent (used internally)
diff --git a/include/rive/virtualizing_component.hpp b/include/rive/virtualizing_component.hpp
index 70b57cb..6a9c680 100644
--- a/include/rive/virtualizing_component.hpp
+++ b/include/rive/virtualizing_component.hpp
@@ -28,6 +28,7 @@
     virtual Vec2D itemSize(int index) = 0;
     virtual void setItemSize(Vec2D size, int index) = 0;
     virtual void addVirtualizable(int index) = 0;
+    virtual void virtualizableChanged() = 0;
     virtual void removeVirtualizable(int index) = 0;
     virtual void setVisibleIndices(int start, int end) = 0;
     virtual void setVirtualizablePosition(int index, Vec2D position) = 0;
diff --git a/src/artboard.cpp b/src/artboard.cpp
index d92c7d4..5ed982f 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -3,6 +3,8 @@
 #include "rive/animation/keyframe_interpolator.hpp"
 #include "rive/artboard_component_list.hpp"
 #include "rive/backboard.hpp"
+#include "rive/component.hpp"
+#include "rive/container_component.hpp"
 #include "rive/focus_data.hpp"
 #include "rive/input/focus_manager.hpp"
 #include "rive/semantic/semantic_data.hpp"
@@ -26,6 +28,7 @@
 #include "rive/importers/import_stack.hpp"
 #include "rive/importers/backboard_importer.hpp"
 #include "rive/layout_component.hpp"
+#include "rive/node.hpp"
 #include "rive/foreground_layout_drawable.hpp"
 #include "rive/nested_artboard.hpp"
 #include "rive/nested_artboard_leaf.hpp"
@@ -1870,6 +1873,82 @@
     return nullptr;
 }
 
+namespace
+{
+// Depth-first in scene (Container child) order so FocusManager child order
+// (tab) matches the hierarchy, not the flat m_Objects / grouped passes.
+// At most one FocusData is allowed as a direct child of a container; it is
+// registered in a first pass, then child traversal skips that FocusData.
+void buildFocusTreeVisit(FocusManager* focusManager,
+                         Component* component,
+                         rcp<FocusNode> focusNode)
+{
+    if (component == nullptr)
+    {
+        return;
+    }
+    if (component->is<NestedArtboard>())
+    {
+        auto* nestedHost = component->as<NestedArtboard>();
+        auto* nestedArtboard = nestedHost->artboardInstance(0);
+        if (nestedArtboard != nullptr &&
+            nestedArtboard->focusManager() != focusManager)
+        {
+            nestedArtboard->cleanupFocusTree();
+            nestedArtboard->buildFocusTree(focusManager, focusNode);
+        }
+        for (auto* animation : nestedHost->nestedAnimations())
+        {
+            if (animation->is<NestedStateMachine>())
+            {
+                auto* nsm = animation->as<NestedStateMachine>();
+                auto* smi = nsm->stateMachineInstance();
+                if (smi != nullptr && smi->focusManager() != focusManager)
+                {
+                    smi->setExternalFocusManager(focusManager);
+                }
+            }
+        }
+    }
+    else if (component->is<ArtboardComponentList>())
+    {
+        auto* list = component->as<ArtboardComponentList>();
+        list->ensureListScopeFocusNode(focusManager, focusNode);
+    }
+    if (component->is<ContainerComponent>())
+    {
+        auto* cc = component->as<ContainerComponent>();
+        FocusData* directFd = nullptr;
+        for (Component* ch : cc->children())
+        {
+            if (ch != nullptr && ch->is<FocusData>())
+            {
+                directFd = ch->as<FocusData>();
+                break;
+            }
+        }
+        rcp<FocusNode> recurseWith;
+        if (directFd != nullptr)
+        {
+            focusManager->addChild(focusNode, directFd->focusNode());
+            recurseWith = directFd->focusNode();
+        }
+        else
+        {
+            recurseWith = focusNode;
+        }
+        for (Component* ch : cc->children())
+        {
+            if (ch == nullptr || ch->is<FocusData>())
+            {
+                continue;
+            }
+            buildFocusTreeVisit(focusManager, ch, recurseWith);
+        }
+    }
+}
+} // namespace
+
 void Artboard::buildFocusTree(FocusManager* focusManager,
                               rcp<FocusNode> parentFocusNode)
 {
@@ -1895,101 +1974,9 @@
     rcp<FocusNode> effectiveParent = parentFocusNode;
 #endif
 
-    // Register all FocusData in this artboard
-    for (auto* obj : m_Objects)
+    if (as<ContainerComponent>() != nullptr)
     {
-        if (obj != nullptr && obj->is<FocusData>())
-        {
-            auto* fd = obj->as<FocusData>();
-            auto* localParent = fd->findParentFocusData();
-
-            rcp<FocusNode> parentNode = localParent != nullptr
-                                            ? localParent->focusNode()
-                                            : effectiveParent;
-
-            focusManager->addChild(parentNode, fd->focusNode());
-        }
-    }
-    // Propagate focus registration to nested artboards that might have been
-    // created before this artboard's focusManager was available. This handles
-    // ArtboardComponentList and NestedArtboard items that were initialized
-    // before the parent StateMachineInstance was created.
-    //
-    // We check if the nested artboard's focusManager is DIFFERENT from ours.
-    // If it is, that means it created its own internal focusManager when it
-    // should be sharing the parent's. We rebuild its focus tree with the
-    // correct shared focusManager.
-
-    // Handle NestedArtboard instances
-    for (auto* nestedArtboardHost : m_NestedArtboards)
-    {
-        // Find closest focus node (handles artboard boundaries)
-        auto hostParentNode =
-            FocusData::findClosestFocusNode(nestedArtboardHost);
-        if (hostParentNode == nullptr)
-        {
-            hostParentNode = effectiveParent;
-        }
-
-        auto* nestedArtboard = nestedArtboardHost->artboardInstance(0);
-        if (nestedArtboard != nullptr &&
-            nestedArtboard->focusManager() != focusManager)
-        {
-            // Clean up old focus tree if it exists (with wrong focusManager)
-            nestedArtboard->cleanupFocusTree();
-            nestedArtboard->buildFocusTree(focusManager, hostParentNode);
-        }
-
-        // Also update the external focus manager on any nested state machines.
-        // This handles the case where initializeAnimation was called before
-        // the parent artboard had a focus manager.
-        for (auto* animation : nestedArtboardHost->nestedAnimations())
-        {
-            if (animation->is<NestedStateMachine>())
-            {
-                auto* nsm = animation->as<NestedStateMachine>();
-                auto* smi = nsm->stateMachineInstance();
-                if (smi != nullptr && smi->focusManager() != focusManager)
-                {
-                    smi->setExternalFocusManager(focusManager);
-                }
-            }
-        }
-    }
-
-    // Handle ArtboardComponentList instances
-    for (auto* componentList : m_ComponentLists)
-    {
-        // Find closest focus node (handles artboard boundaries)
-        auto hostParentNode = FocusData::findClosestFocusNode(componentList);
-        if (hostParentNode == nullptr)
-        {
-            hostParentNode = effectiveParent;
-        }
-
-        for (size_t i = 0; i < componentList->artboardCount(); i++)
-        {
-            auto* nestedArtboard =
-                componentList->artboardInstance(static_cast<int>(i));
-            if (nestedArtboard != nullptr &&
-                nestedArtboard->focusManager() != focusManager)
-            {
-                // Clean up old focus tree if it exists (with wrong
-                // focusManager)
-                nestedArtboard->cleanupFocusTree();
-                nestedArtboard->buildFocusTree(focusManager, hostParentNode);
-            }
-
-            // Also update the state machine's external focus manager.
-            // This handles the case where linkStateMachine was called before
-            // the parent artboard had a focus manager.
-            auto* smi =
-                componentList->stateMachineInstance(static_cast<int>(i));
-            if (smi != nullptr && smi->focusManager() != focusManager)
-            {
-                smi->setExternalFocusManager(focusManager);
-            }
-        }
+        buildFocusTreeVisit(focusManager, this, std::move(effectiveParent));
     }
 }
 
@@ -2057,6 +2044,11 @@
         }
     }
 
+    for (auto* componentList : m_ComponentLists)
+    {
+        componentList->removeListScopeFocusNode();
+    }
+
     // Clear the active focus manager reference
     m_activeFocusManager = nullptr;
 }
diff --git a/src/artboard_component_list.cpp b/src/artboard_component_list.cpp
index f80b773..6b7c179 100644
--- a/src/artboard_component_list.cpp
+++ b/src/artboard_component_list.cpp
@@ -120,6 +120,8 @@
             artboard.second->cleanupFocusTree();
         }
     }
+    removeListScopeFocusNode();
+    m_listRowFocusNodes.clear();
 
     // Destroy state machines BEFORE artboards.
     // StateMachineInstance owns FocusListenerGroup objects that hold raw
@@ -371,45 +373,265 @@
     return nullptr;
 }
 
+void ArtboardComponentList::ensureListScopeFocusNode(FocusManager* focusManager,
+                                                     rcp<FocusNode> hostParent)
+{
+    if (focusManager == nullptr)
+    {
+        return;
+    }
+    if (m_listScopeFocusNode == nullptr)
+    {
+        m_listScopeFocusNode = rcp<FocusNode>(new FocusNode(nullptr));
+        m_listScopeFocusNode->canFocus(true);
+        m_listScopeFocusNode->canTraverse(true);
+        m_listScopeFocusNode->name("ArtboardComponentListScope");
+    }
+    if (m_listScopeFocusNode->manager() == focusManager)
+    {
+        if (m_listScopeFocusNode->parent() == hostParent.get() ||
+            (m_listScopeFocusNode->parent() == nullptr &&
+             hostParent == nullptr))
+        {
+            syncListRowNodesWithList(focusManager);
+            return;
+        }
+    }
+    focusManager->addChild(std::move(hostParent), m_listScopeFocusNode);
+    syncListRowNodesWithList(focusManager);
+}
+
+void ArtboardComponentList::removeListScopeFocusNode()
+{
+    for (size_t r = 0; r < m_listRowFocusNodes.size(); r++)
+    {
+        auto& row = m_listRowFocusNodes[r];
+        if (row == nullptr)
+        {
+            continue;
+        }
+        if (row->manager() != nullptr)
+        {
+            row->manager()->removeChild(row);
+        }
+        row.reset();
+    }
+    m_listRowFocusNodes.clear();
+    if (m_listScopeFocusNode == nullptr)
+    {
+        return;
+    }
+    if (m_listScopeFocusNode->manager() != nullptr)
+    {
+        m_listScopeFocusNode->manager()->removeChild(m_listScopeFocusNode);
+    }
+    m_listScopeFocusNode.reset();
+}
+
+rcp<FocusNode> ArtboardComponentList::makeListRowFocusNode() const
+{
+    auto node = rcp<FocusNode>(new FocusNode(nullptr));
+    node->canFocus(true);
+    node->canTraverse(true);
+    node->name("ArtboardComponentListRow");
+    return node;
+}
+
+void ArtboardComponentList::reparentListRowsInScope(FocusManager* fm)
+{
+    if (fm == nullptr || m_listScopeFocusNode == nullptr)
+    {
+        return;
+    }
+    for (size_t i = 0; i < m_listRowFocusNodes.size(); i++)
+    {
+        rcp<FocusNode> row = m_listRowFocusNodes[i];
+        if (row == nullptr)
+        {
+            continue;
+        }
+        if (row->parent() != nullptr)
+        {
+            row->removeFromParent();
+        }
+    }
+    for (size_t i = 0; i < m_listRowFocusNodes.size(); i++)
+    {
+        rcp<FocusNode> row = m_listRowFocusNodes[i];
+        if (row == nullptr)
+        {
+            continue;
+        }
+        fm->addChild(m_listScopeFocusNode, row, i);
+    }
+}
+
+bool ArtboardComponentList::listItemNeedsBuildUnderRow(FocusManager* parentFM,
+                                                       ArtboardInstance* inst,
+                                                       rcp<FocusNode> row) const
+{
+    if (parentFM == nullptr || inst == nullptr || row == nullptr)
+    {
+        return false;
+    }
+    if (inst->focusManager() != parentFM)
+    {
+        return true;
+    }
+    // If the artboard has focusables but the row is empty, focus is still
+    // attached under the list scope (legacy) and must be rebuilt on the row.
+    if (row->children().empty() && inst->rootFocusDataCount() > 0)
+    {
+        return true;
+    }
+    return false;
+}
+
+void ArtboardComponentList::syncListRowNodesWithList(FocusManager* fm)
+{
+    if (m_listItems.empty())
+    {
+        while (!m_listRowFocusNodes.empty())
+        {
+            auto row = m_listRowFocusNodes.back();
+            m_listRowFocusNodes.pop_back();
+            if (row != nullptr && row->manager() != nullptr)
+            {
+                row->manager()->removeChild(row);
+            }
+        }
+        return;
+    }
+    std::vector<rcp<ViewModelInstanceListItem>> listCopy = m_listItems;
+    std::vector<rcp<FocusNode>> rowCopy = m_listRowFocusNodes;
+    syncListRowNodesWithList(fm, listCopy, rowCopy);
+}
+
+void ArtboardComponentList::syncListRowNodesWithList(
+    FocusManager* fm,
+    const std::vector<rcp<ViewModelInstanceListItem>>& previousListItems,
+    const std::vector<rcp<FocusNode>>& previousRowNodes)
+{
+    if (fm == nullptr || m_listScopeFocusNode == nullptr)
+    {
+        return;
+    }
+    const int n = static_cast<int>(m_listItems.size());
+    if (n == 0)
+    {
+        m_listRowFocusNodes.clear();
+        return;
+    }
+
+    std::vector<rcp<FocusNode>> newRows(static_cast<size_t>(n), nullptr);
+    std::vector<rcp<FocusNode>> pr = previousRowNodes;
+    for (int j = 0; j < n; j++)
+    {
+        for (int k = 0; k < static_cast<int>(previousListItems.size()) &&
+                        k < static_cast<int>(pr.size());
+             k++)
+        {
+            if (m_listItems[static_cast<size_t>(j)] ==
+                previousListItems[static_cast<size_t>(k)])
+            {
+                newRows[static_cast<size_t>(j)] = pr[static_cast<size_t>(k)];
+                pr[static_cast<size_t>(k)] = nullptr;
+                break;
+            }
+        }
+    }
+    for (int k = 0; k < static_cast<int>(pr.size()) &&
+                    k < static_cast<int>(previousListItems.size());
+         k++)
+    {
+        rcp<FocusNode> unmapped = pr[static_cast<size_t>(k)];
+        if (unmapped == nullptr)
+        {
+            continue;
+        }
+        bool inNew = false;
+        for (int j = 0; j < n; j++)
+        {
+            if (m_listItems[static_cast<size_t>(j)] ==
+                previousListItems[static_cast<size_t>(k)])
+            {
+                inNew = true;
+                break;
+            }
+        }
+        if (!inNew)
+        {
+            if (unmapped->manager() != nullptr)
+            {
+                unmapped->manager()->removeChild(unmapped);
+            }
+        }
+    }
+    m_listRowFocusNodes = std::move(newRows);
+    for (int i = 0; i < n; i++)
+    {
+        rcp<FocusNode>& row = m_listRowFocusNodes[static_cast<size_t>(i)];
+        if (row == nullptr)
+        {
+            row = makeListRowFocusNode();
+        }
+    }
+    reparentListRowsInScope(fm);
+    for (int i = 0; i < n; i++)
+    {
+        ArtboardInstance* inst = artboardInstance(i);
+        if (inst == nullptr)
+        {
+            continue;
+        }
+        rcp<FocusNode> row = m_listRowFocusNodes[static_cast<size_t>(i)];
+        if (row == nullptr)
+        {
+            continue;
+        }
+        if (listItemNeedsBuildUnderRow(fm, inst, row))
+        {
+            if (inst->focusManager() != nullptr)
+            {
+                inst->cleanupFocusTree();
+            }
+            inst->buildFocusTree(fm, row);
+        }
+        auto* smi = stateMachineInstance(i);
+        if (smi != nullptr && smi->focusManager() != fm)
+        {
+            smi->setExternalFocusManager(fm);
+        }
+    }
+}
+
 void ArtboardComponentList::linkStateMachineToArtboard(
     StateMachineInstance* stateMachineInstance,
     ArtboardInstance* artboardInstance)
 {
-    if (artboardInstance != nullptr && stateMachineInstance != nullptr)
+    if (artboardInstance == nullptr || stateMachineInstance == nullptr)
     {
-        auto dataContext = artboardInstance->dataContext();
-        stateMachineInstance->dataContext(dataContext);
-        // TODO: @hernan added this to make sure data binds are procesed in the
-        // current frame instead of waiting for the next run. But might not be
-        // necessary. Needs more testing.
-        stateMachineInstance->updateDataBinds(false);
+        return;
+    }
+    auto dataContext = artboardInstance->dataContext();
+    stateMachineInstance->dataContext(dataContext);
+    stateMachineInstance->updateDataBinds(false);
 
-        // Share parent artboard's focus manager and build focus tree for list
-        // item.
-        auto* parentArtboard = this->artboard();
-        if (parentArtboard != nullptr &&
-            parentArtboard->focusManager() != nullptr)
-        {
-            auto* parentFM = parentArtboard->focusManager();
-            stateMachineInstance->setExternalFocusManager(parentFM);
+    auto* parentArtboard = this->artboard();
+    if (parentArtboard != nullptr && parentArtboard->focusManager() != nullptr)
+    {
+        auto* parentFM = parentArtboard->focusManager();
+        stateMachineInstance->setExternalFocusManager(parentFM);
+    }
 
-            // Find closest focus node (handles artboard boundaries)
-            auto parentNode = FocusData::findClosestFocusNode(this);
-
-            // Build list item's focus tree under parent
-            artboardInstance->buildFocusTree(parentFM, parentNode);
-        }
-
-        // Share parent artboard's semantic manager and build semantic tree
-        // reparented under the enclosing SemanticData.
-        if (parentArtboard != nullptr &&
-            parentArtboard->semanticManager() != nullptr)
-        {
-            auto* parentSM = parentArtboard->semanticManager();
-            auto parentNode = SemanticData::findClosestSemanticNode(this);
-            stateMachineInstance->setExternalSemanticManager(parentSM,
-                                                             parentNode);
-        }
+    // Share parent artboard's semantic manager and build semantic tree
+    // reparented under the enclosing SemanticData.
+    if (parentArtboard != nullptr &&
+        parentArtboard->semanticManager() != nullptr)
+    {
+        auto* parentSM = parentArtboard->semanticManager();
+        auto parentNode = SemanticData::findClosestSemanticNode(this);
+        stateMachineInstance->setExternalSemanticManager(parentSM, parentNode);
     }
 }
 
@@ -444,6 +666,9 @@
     {
         return;
     }
+    const std::vector<rcp<ViewModelInstanceListItem>> preListItems =
+        m_listItems;
+    const std::vector<rcp<FocusNode>> preRowNodes = m_listRowFocusNodes;
     m_oldItems.clear();
     m_oldItems.assign(m_listItems.begin(), m_listItems.end());
     m_listItems.clear();
@@ -525,6 +750,14 @@
     addDirt(ComponentDirt::Components);
     recomputeListUsesDrawIndexSort();
     syncDrawIndexListeners();
+    auto* parentAb = artboard();
+    if (parentAb != nullptr && parentAb->focusManager() != nullptr &&
+        m_listScopeFocusNode != nullptr)
+    {
+        syncListRowNodesWithList(parentAb->focusManager(),
+                                 preListItems,
+                                 preRowNodes);
+    }
 }
 
 void ArtboardComponentList::syncLayoutChildren()
@@ -1404,6 +1637,19 @@
     }
 }
 
+void ArtboardComponentList::virtualizableChanged()
+{
+    auto* parentArtboard = this->artboard();
+    if (parentArtboard != nullptr && parentArtboard->focusManager() != nullptr)
+    {
+        auto* parentFM = parentArtboard->focusManager();
+        if (m_listScopeFocusNode != nullptr)
+        {
+            syncListRowNodesWithList(parentFM);
+        }
+    }
+}
+
 void ArtboardComponentList::removeVirtualizable(int index)
 {
     auto listItem = this->listItem(index);
diff --git a/src/constraints/scrolling/scroll_virtualizer.cpp b/src/constraints/scrolling/scroll_virtualizer.cpp
index 3ffd2a3..a35aa40 100644
--- a/src/constraints/scrolling/scroll_virtualizer.cpp
+++ b/src/constraints/scrolling/scroll_virtualizer.cpp
@@ -1,6 +1,7 @@
 #include "rive/constraints/scrolling/scroll_constraint.hpp"
 #include "rive/layout/layout_node_provider.hpp"
 #include "rive/constraints/scrolling/scroll_virtualizer.hpp"
+#include <set>
 
 using namespace rive;
 
@@ -79,6 +80,7 @@
     int currentChildIndex = 0;
     bool isHorz = m_direction == VirtualizedDirection::horizontal;
     float gap = isHorz ? scroll->gap().x : scroll->gap().y;
+    std::set<VirtualizingComponent*> changedVirtualizingComponents;
 
     for (int i = 0; i < children.size(); i++)
     {
@@ -269,6 +271,7 @@
                         if (item == nullptr)
                         {
                             virt->addVirtualizable(childIndex);
+                            changedVirtualizingComponents.emplace(virt);
                         }
 
                         auto size = getItemSize(child, childIndex, isHorz);
@@ -322,6 +325,10 @@
             }
         }
     }
+    for (auto& virtualizingComponent : changedVirtualizingComponents)
+    {
+        virtualizingComponent->virtualizableChanged();
+    }
 }
 
 void ScrollVirtualizer::recycleItems(std::vector<int> indices,
diff --git a/src/input/focus_manager.cpp b/src/input/focus_manager.cpp
index f31c241..23399f0 100644
--- a/src/input/focus_manager.cpp
+++ b/src/input/focus_manager.cpp
@@ -9,6 +9,7 @@
 #include "rive/focus_data.hpp"
 #include "rive/math/aabb.hpp"
 #include <algorithm>
+#include <cstddef>
 #include <cmath>
 #include <limits>
 #include <unordered_set>
@@ -185,6 +186,42 @@
     }
 }
 
+void FocusManager::addChild(rcp<FocusNode> parent,
+                            rcp<FocusNode> child,
+                            size_t index)
+{
+    if (!child)
+    {
+        return;
+    }
+    if (child->parent())
+    {
+        child->removeFromParent();
+    }
+    else
+    {
+        auto it = std::find(m_rootNodes.begin(), m_rootNodes.end(), child);
+        if (it != m_rootNodes.end())
+        {
+            m_rootNodes.erase(it);
+        }
+    }
+    child->m_manager = this;
+    if (parent)
+    {
+        parent->insertChild(index, std::move(child));
+    }
+    else
+    {
+        if (index > m_rootNodes.size())
+        {
+            index = m_rootNodes.size();
+        }
+        m_rootNodes.insert(m_rootNodes.begin() + static_cast<ptrdiff_t>(index),
+                           std::move(child));
+    }
+}
+
 void FocusManager::removeChild(rcp<FocusNode> child)
 {
     if (!child)
diff --git a/src/input/focus_node.cpp b/src/input/focus_node.cpp
index 8e30df6..a97eb85 100644
--- a/src/input/focus_node.cpp
+++ b/src/input/focus_node.cpp
@@ -4,6 +4,7 @@
 
 #include "rive/input/focus_node.hpp"
 #include <algorithm>
+#include <cstddef>
 
 namespace rive
 {
@@ -22,6 +23,22 @@
     m_children.push_back(std::move(child));
 }
 
+void FocusNode::insertChild(size_t index, rcp<FocusNode> child)
+{
+    if (!child)
+    {
+        return;
+    }
+    child->removeFromParent();
+    child->m_parent = this;
+    if (index > m_children.size())
+    {
+        index = m_children.size();
+    }
+    m_children.insert(m_children.begin() + static_cast<ptrdiff_t>(index),
+                      std::move(child));
+}
+
 void FocusNode::removeChild(rcp<FocusNode> child)
 {
     if (!child || child->m_parent != this)
diff --git a/tests/unit_tests/assets/list_focus_order.riv b/tests/unit_tests/assets/list_focus_order.riv
new file mode 100644
index 0000000..bbb376d
--- /dev/null
+++ b/tests/unit_tests/assets/list_focus_order.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/focus_test.cpp b/tests/unit_tests/runtime/focus_test.cpp
index 7e8c50a..5be91cb 100644
--- a/tests/unit_tests/runtime/focus_test.cpp
+++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -4,6 +4,9 @@
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_machine_instance.hpp"
 #include "rive/artboard.hpp"
+#include "rive/artboard_component_list.hpp"
+#include "rive/focus_data.hpp"
+#include "rive/node.hpp"
 #include "rive/input/focus_node.hpp"
 #include "rive/input/focus_manager.hpp"
 #include "utils/no_op_factory.hpp"
@@ -291,6 +294,34 @@
     CHECK(focusable.blurredCount == 1);
 }
 
+TEST_CASE(
+    "List row reparent: FocusNode removeFromParent preserves primary focus",
+    "[FocusManager][list]")
+{
+    FocusManager manager;
+    MockFocusable fLeaf;
+    auto scope = make_rcp<FocusNode>(nullptr);
+    scope->canFocus(true);
+    scope->canTraverse(true);
+    auto row = make_rcp<FocusNode>(nullptr);
+    row->canFocus(true);
+    row->canTraverse(true);
+    auto leaf = make_rcp<FocusNode>(&fLeaf);
+
+    manager.addChild(nullptr, scope);
+    manager.addChild(scope, row);
+    manager.addChild(row, leaf);
+    manager.setFocus(leaf);
+    CHECK(manager.primaryFocus() == leaf);
+
+    row->removeFromParent();
+    CHECK(manager.primaryFocus() == leaf);
+
+    manager.addChild(scope, row, 0);
+    CHECK(manager.primaryFocus() == leaf);
+    CHECK(fLeaf.blurredCount == 0);
+}
+
 TEST_CASE("FocusManager input routing", "[FocusManager]")
 {
     FocusManager manager;
@@ -970,18 +1001,21 @@
                                         ->as<rive::ViewModelInstanceBoolean>();
 
     stateMachine->bindViewModelInstance(vmi);
+    // ===> Frame 0
     auto renderer = silver.makeRenderer();
     stateMachine->advanceAndApply(0.016f);
+    // ===> Frame 1
     artboard->draw(renderer.get());
     silver.addFrame();
 
     focusManager->focusNext();
+    focusManager->focusNext();
     // Focus on an element
     REQUIRE(focusManager->primaryFocus() != nullptr);
     stateMachine->advanceAndApply(0.016f);
     artboard->draw(renderer.get());
+    // ===> Frame 2
     silver.addFrame();
-
     // Hide the element, ensure the focus has been dropped
     opacityProp->propertyValue(0);
     // First advance sets the opacity to 0
@@ -990,33 +1024,40 @@
     stateMachine->advanceAndApply(0.016f);
     REQUIRE(focusManager->primaryFocus() == nullptr);
     artboard->draw(renderer.get());
+    // ===> Frame 3
     silver.addFrame();
 
     opacityProp->propertyValue(1);
     stateMachine->advanceAndApply(0.016f);
     artboard->draw(renderer.get());
+    // ===> Frame 4
     silver.addFrame();
     focusManager->focusNext();
+    focusManager->focusNext();
     stateMachine->advanceAndApply(0.016f);
     REQUIRE(focusManager->primaryFocus() != nullptr);
     artboard->draw(renderer.get());
+    // ===> Frame 5
     silver.addFrame();
     isMainLayout2VisibleProp->propertyValue(false);
     stateMachine->advanceAndApply(0.016f);
     focusManager->focusNext();
     stateMachine->advanceAndApply(0.016f);
     artboard->draw(renderer.get());
+    // ===> Frame 6
     silver.addFrame();
 
     // Toggles only between visible focused elements
     focusManager->focusNext();
     stateMachine->advanceAndApply(0.016f);
     artboard->draw(renderer.get());
+    // ===> Frame 7
     silver.addFrame();
     focusManager->focusNext();
     focusManager->focusNext();
     stateMachine->advanceAndApply(0.016f);
     artboard->draw(renderer.get());
+    // ===> Frame 8
     silver.addFrame();
 
     // Fully rotates over all nodes
@@ -1024,22 +1065,27 @@
     stateMachine->advanceAndApply(0.016f);
     focusManager->focusNext();
     artboard->draw(renderer.get());
+    // ===> Frame 9
     silver.addFrame();
     stateMachine->advanceAndApply(0.016f);
     focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    // ===> Frame 10
+    silver.addFrame();
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    focusManager->focusNext();
     artboard->draw(renderer.get());
+    // ===> Frame 11
     silver.addFrame();
+    focusManager->focusNext();
     stateMachine->advanceAndApply(0.016f);
-    focusManager->focusNext();
-    focusManager->focusNext();
     artboard->draw(renderer.get());
+    // ===> Frame 12
     silver.addFrame();
-    stateMachine->advanceAndApply(0.016f);
     focusManager->focusNext();
-    artboard->draw(renderer.get());
-    silver.addFrame();
     stateMachine->advanceAndApply(0.016f);
-    focusManager->focusNext();
     artboard->draw(renderer.get());
 
     CHECK(silver.matches("focus_collapsing"));
@@ -1420,4 +1466,153 @@
     artboard->draw(renderer.get());
 
     CHECK(silver.matches("focusable_element"));
+}
+
+TEST_CASE("ArtboardComponentList list scope is registered on shared "
+          "FocusManager",
+          "[FocusManager][list]")
+{
+    auto file = ReadRiveFile("assets/component_list_1.riv");
+    auto artboard = file->artboard("Main")->instance();
+    REQUIRE(artboard != nullptr);
+    auto vmi = file->createDefaultViewModelInstance(artboard.get());
+    REQUIRE(vmi != nullptr);
+    artboard->bindViewModelInstance(vmi);
+    auto sm = artboard->stateMachineAt(0);
+    REQUIRE(sm != nullptr);
+    artboard->advance(0.0f);
+
+    auto* list = artboard->find<rive::ArtboardComponentList>("List");
+    REQUIRE(list != nullptr);
+    auto* fm = artboard->focusManager();
+    REQUIRE(fm != nullptr);
+
+    artboard->buildFocusTree(artboard->focusManager(), nullptr);
+    auto scope = list->listScopeFocusNode();
+    REQUIRE(scope != nullptr);
+    CHECK(scope->manager() == fm);
+    CHECK(scope->name() == "ArtboardComponentListScope");
+    CHECK(scope->canFocus() == true);
+    CHECK(scope->canTraverse() == true);
+    CHECK(scope->focusable() == nullptr);
+}
+
+TEST_CASE("List under Node: when parent has a direct FocusData, "
+          "findClosestFocusNode from list matches that node",
+          "[FocusManager][list]")
+{
+    // buildFocusTreeVisit pass-1: at most one direct child FocusData per
+    // container; if present, its focusNode is the scope for siblings (e.g. the
+    // list host). The walk-based fallback from the old findClosest for the
+    // no-direct-FocusData case is not used by the focus build anymore.
+    auto file = ReadRiveFile("assets/component_list_1.riv");
+    auto artboard = file->artboard("Main")->instance();
+    REQUIRE(artboard != nullptr);
+    auto vmi = file->createDefaultViewModelInstance(artboard.get());
+    REQUIRE(vmi != nullptr);
+    artboard->bindViewModelInstance(vmi);
+    auto sm = artboard->stateMachineAt(0);
+    REQUIRE(sm != nullptr);
+    artboard->advance(0.0f);
+
+    auto* list = artboard->find<rive::ArtboardComponentList>("List");
+    REQUIRE(list != nullptr);
+    auto* p = list->parent();
+    REQUIRE(p != nullptr);
+    REQUIRE(p->is<rive::Node>());
+
+    rive::rcp<rive::FocusNode> fromFirstDirectFd;
+    for (auto* ch : p->as<rive::Node>()->children())
+    {
+        if (ch != nullptr && ch->is<rive::FocusData>())
+        {
+            fromFirstDirectFd = ch->as<rive::FocusData>()->focusNode();
+            break;
+        }
+    }
+    if (fromFirstDirectFd != nullptr)
+    {
+        CHECK(rive::FocusData::findClosestFocusNode(list) == fromFirstDirectFd);
+    }
+}
+
+TEST_CASE("Focus is correctly built and updated for lists", "[silver]")
+{
+    rive::SerializingFactory silver;
+    auto file = ReadRiveFile("assets/list_focus_order.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 stageProcessedProp = vmi->propertyValue("stageProcessed")
+                                  ->as<rive::ViewModelInstanceBoolean>();
+    auto stageCountProp =
+        vmi->propertyValue("stageCount")->as<rive::ViewModelInstanceNumber>();
+
+    auto renderer = silver.makeRenderer();
+    stateMachine->bindViewModelInstance(vmi);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focuses on first element of tree
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focuses on last element of list
+    focusManager->focusNext();
+    focusManager->focusNext();
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Inserts one element at end of list
+    stageProcessedProp->propertyValue(false);
+    stageCountProp->propertyValue(1);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focus is on that new element
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focused elements is moved in the list and keeps focus
+    stageProcessedProp->propertyValue(false);
+    stageCountProp->propertyValue(2);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focusing on the next element correctly focuses on the next element on the
+    // list
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Removing the focused element from the list, clears the focus
+    stageProcessedProp->propertyValue(false);
+    stageCountProp->propertyValue(3);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+    silver.addFrame();
+
+    // Focuses back on first element of tree
+    focusManager->focusNext();
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    CHECK(silver.matches("list_focus_order"));
 }
\ No newline at end of file
diff --git a/tests/unit_tests/silvers/focus_collapsing.sriv b/tests/unit_tests/silvers/focus_collapsing.sriv
index a3ee9b7..04e33a7 100644
--- a/tests/unit_tests/silvers/focus_collapsing.sriv
+++ b/tests/unit_tests/silvers/focus_collapsing.sriv
Binary files differ
diff --git a/tests/unit_tests/silvers/list_focus_order.sriv b/tests/unit_tests/silvers/list_focus_order.sriv
new file mode 100644
index 0000000..046ebf0
--- /dev/null
+++ b/tests/unit_tests/silvers/list_focus_order.sriv
Binary files differ