--- a/net/minecraft/world/ticks/LevelTicks.java
+++ b/net/minecraft/world/ticks/LevelTicks.java
@@ -39,12 +_,69 @@
     private final List<ScheduledTick<T>> alreadyRunThisTick = new ArrayList<>();
     private final Set<ScheduledTick<?>> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH);
     private final BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> chunkScheduleUpdater = (levelChunkTicks, scheduledTick) -> {
-        if (scheduledTick.equals(levelChunkTicks.peek())) {
-            this.updateContainerScheduling(scheduledTick);
+        if (scheduledTick.equals(levelChunkTicks.peek())) { // Folia - diff on change
+            this.updateContainerScheduling(scheduledTick); // Folia - diff on change
         }
     };
 
-    public LevelTicks(LongPredicate tickCheck) {
+    // Folia start - region threading
+    public final net.minecraft.server.level.ServerLevel world;
+    public final boolean isBlock;
+
+    public void merge(final LevelTicks<T> into, final long tickOffset) {
+        // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet
+        // are all transient state, only ever non-empty during tick. But merging regions occurs while there
+        // is no tick happening, so we assume they are empty.
+        for (final java.util.Iterator<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
+             ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
+             iterator.hasNext();) {
+            final Long2ObjectMap.Entry<LevelChunkTicks<T>> entry = iterator.next();
+            final LevelChunkTicks<T> tickContainer = entry.getValue();
+            tickContainer.offsetTicks(tickOffset);
+            into.allContainers.put(entry.getLongKey(), tickContainer);
+        }
+        for (final java.util.Iterator<Long2LongMap.Entry> iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator();
+             iterator.hasNext();) {
+            final Long2LongMap.Entry entry = iterator.next();
+            into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset);
+        }
+    }
+
+    public void split(final int chunkToRegionShift,
+                      final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap<LevelTicks<T>> regionToData) {
+        for (final java.util.Iterator<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
+             ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
+             iterator.hasNext();) {
+            final Long2ObjectMap.Entry<LevelChunkTicks<T>> entry = iterator.next();
+
+            final long chunkKey = entry.getLongKey();
+            final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey);
+            final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey);
+
+            final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(
+                    chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift
+            );
+            // Should always be non-null, since containers are removed on unload.
+            regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue());
+        }
+        for (final java.util.Iterator<Long2LongMap.Entry> iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator();
+             iterator.hasNext();) {
+            final Long2LongMap.Entry entry = iterator.next();
+            final long chunkKey = entry.getLongKey();
+            final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey);
+            final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey);
+
+            final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(
+                    chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift
+            );
+
+            // Should always be non-null, since containers are removed on unload.
+            regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue());
+        }
+    }
+    // Folia end - region threading
+
+    public LevelTicks(LongPredicate tickCheck, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock
         this.tickCheck = tickCheck;
     }
 
@@ -56,7 +_,17 @@
             this.nextTickForContainer.put(packedChunkPos, scheduledTick.triggerTick());
         }
 
-        chunkTicks.setOnTickAdded(this.chunkScheduleUpdater);
+        // Folia start - region threading
+        final boolean isBlock = this.isBlock;
+        final net.minecraft.server.level.ServerLevel world = this.world;
+        // make sure the lambda contains no reference to this LevelTicks
+        chunkTicks.setOnTickAdded((LevelChunkTicks<T> levelChunkTicks, ScheduledTick<T> tick) -> {
+            if (tick.equals(levelChunkTicks.peek())) {
+                io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData();
+                ((LevelTicks<T>)(isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks())).updateContainerScheduling(tick);
+            }
+        });
+        // Folia end - region threading
     }
 
     public void removeContainer(ChunkPos chunkPos) {
@@ -70,6 +_,7 @@
 
     @Override
     public void schedule(ScheduledTick<T> tick) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, tick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading
         long packedChunkPos = ChunkPos.asLong(tick.pos());
         LevelChunkTicks<T> levelChunkTicks = this.allContainers.get(packedChunkPos);
         if (levelChunkTicks == null) {
