diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD
index e3f895d56d..d850ac9f2d 100644
--- a/submodules/ChatSendMessageActionUI/BUILD
+++ b/submodules/ChatSendMessageActionUI/BUILD
@@ -32,6 +32,8 @@ swift_library(
         "//submodules/WallpaperBackgroundNode",
         "//submodules/Components/MultilineTextWithEntitiesComponent",
         "//submodules/Components/MultilineTextComponent",
+        "//submodules/TelegramUI/Components/LottieMetal",
+        "//submodules/TelegramAnimatedStickerNode",
     ],
     visibility = [
         "//visibility:public",
diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift
index ad03b16d00..7513d56c5b 100644
--- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift
+++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift
@@ -16,6 +16,8 @@ import ComponentDisplayAdapters
 import WallpaperBackgroundNode
 import ReactionSelectionNode
 import EntityKeyboard
+import LottieMetal
+import TelegramAnimatedStickerNode
 
 func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
     let sourceWindowFrame = fromView.convert(frame, to: nil)
@@ -127,7 +129,7 @@ final class ChatSendMessageContextScreenComponent: Component {
         
         private let messageEffectDisposable = MetaDisposable()
         private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
-        private var standaloneReactionAnimation: StandaloneReactionAnimation?
+        private var standaloneReactionAnimation: LottieMetalAnimatedStickerNode?
         
         private var presentationAnimationState: PresentationAnimationState = .initial
         private var appliedAnimationState: PresentationAnimationState = .initial
@@ -509,7 +511,7 @@ final class ChatSendMessageContextScreenComponent: Component {
                             ReactionContextNode.randomGenericReactionEffect(context: component.context)
                         )
                         |> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
-                            guard let self, let component = self.component, let environment = self.environment else {
+                            guard let self, let component = self.component else {
                                 return
                             }
                             guard let messageEffect else {
@@ -517,17 +519,6 @@ final class ChatSendMessageContextScreenComponent: Component {
                             }
                             let effectId = messageEffect.id
                             
-                            let reactionItem = ReactionItem(
-                                reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction),
-                                appearAnimation: messageEffect.effectSticker,
-                                stillAnimation: messageEffect.effectSticker,
-                                listAnimation: messageEffect.effectSticker,
-                                largeListAnimation: messageEffect.effectSticker,
-                                applicationAnimation: nil,
-                                largeApplicationAnimation: nil,
-                                isCustom: true
-                            )
-                            
                             if let selectedMessageEffect = self.selectedMessageEffect {
                                 if selectedMessageEffect.id == effectId {
                                     self.selectedMessageEffect = nil
@@ -570,12 +561,7 @@ final class ChatSendMessageContextScreenComponent: Component {
                                 })
                             }
                             
-                            let genericReactionEffect = path
-                            
-                            let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect)
-                            standaloneReactionAnimation.frame = self.bounds
-                            self.standaloneReactionAnimation = standaloneReactionAnimation
-                            self.addSubnode(standaloneReactionAnimation)
+                            let _ = path
                             
                             var customEffectResource: MediaResource?
                             if let effectAnimation = messageEffect.effectAnimation {
@@ -586,24 +572,34 @@ final class ChatSendMessageContextScreenComponent: Component {
                                     customEffectResource = effectFile.resource
                                 }
                             }
+                            guard let customEffectResource else {
+                                return
+                            }
                             
-                            standaloneReactionAnimation.animateReactionSelection(
-                                context: component.context,
-                                theme: environment.theme,
-                                animationCache: component.context.animationCache,
-                                reaction: reactionItem,
-                                customEffectResource: customEffectResource,
-                                avatarPeers: [],
-                                playHaptic: true,
-                                isLarge: true,
-                                playCenterReaction: false,
-                                targetView: targetView,
-                                addStandaloneReactionAnimation: { _ in
-                                },
-                                completion: { [weak standaloneReactionAnimation] in
-                                    standaloneReactionAnimation?.removeFromSupernode()
+                            let standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
+                            standaloneReactionAnimation.isUserInteractionEnabled = false
+                            let effectSize = CGSize(width: 380.0, height: 380.0)
+                            var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
+                            effectFrame.origin.x -= effectFrame.width * 0.3
+                            self.standaloneReactionAnimation = standaloneReactionAnimation
+                            standaloneReactionAnimation.frame = effectFrame
+                            standaloneReactionAnimation.updateLayout(size: effectFrame.size)
+                            self.addSubnode(standaloneReactionAnimation)
+                            
+                            let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
+                            standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
+                            standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
+                                guard let self else {
+                                    return
                                 }
-                            )
+                                if let standaloneReactionAnimation {
+                                    standaloneReactionAnimation.removeFromSupernode()
+                                    if self.standaloneReactionAnimation === standaloneReactionAnimation {
+                                        self.standaloneReactionAnimation = nil
+                                    }
+                                }
+                            }
+                            standaloneReactionAnimation.visibility = true
                         }))
                     }
                     reactionContextNode.displayTail = true
diff --git a/submodules/MetalEngine/Sources/MetalEngine.swift b/submodules/MetalEngine/Sources/MetalEngine.swift
index b34ca53a79..7fc7064245 100644
--- a/submodules/MetalEngine/Sources/MetalEngine.swift
+++ b/submodules/MetalEngine/Sources/MetalEngine.swift
@@ -347,6 +347,7 @@ public final class MetalEngineSubjectContext {
     fileprivate var computeOperations: [ComputeOperation] = []
     fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
     fileprivate var freeResourcesOnCompletion: [MetalEngineResource] = []
+    fileprivate var customCompletions: [() -> Void] = []
     
     fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) {
         self.device = device
@@ -446,6 +447,10 @@ public final class MetalEngineSubjectContext {
             return commands(commandBuffer, state)
         })
     }
+    
+    public func addCustomCompletion(_ customCompletion: @escaping () -> Void) {
+        self.customCompletions.append(customCompletion)
+    }
 }
 
 public final class MetalEngineSubjectInternalData {
@@ -1039,13 +1044,17 @@ public final class MetalEngine {
                 }
             }
             
-            if !subjectContext.freeResourcesOnCompletion.isEmpty {
+            if !subjectContext.freeResourcesOnCompletion.isEmpty || !subjectContext.customCompletions.isEmpty {
                 let freeResourcesOnCompletion = subjectContext.freeResourcesOnCompletion
+                let customCompletions = subjectContext.customCompletions
                 commandBuffer.addCompletedHandler { _ in
                     DispatchQueue.main.async {
                         for resource in freeResourcesOnCompletion {
                             resource.free()
                         }
+                        for customCompletion in customCompletions {
+                            customCompletion()
+                        }
                     }
                 }
             }
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
index c534befd47..750e216f82 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
@@ -5793,7 +5793,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
         
         let source = AnimatedStickerResourceSource(account: item.context.account, resource: resource, fitzModifier: nil)
         
-        let animationSize = CGSize(width: 180.0, height: 180.0)
+        let animationSize = CGSize(width: 380.0, height: 380.0)
         let animationNodeFrame: CGRect
         
         var messageEffectView: UIView?
@@ -5825,18 +5825,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
             let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
             
             let additionalAnimationNode = LottieMetalAnimatedStickerNode()
+            additionalAnimationNode.updateLayout(size: animationSize)
             additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 1.6), height: Int(animationSize.height * 1.6), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
             var animationFrame: CGRect
             if isStickerEffect {
-                let scale: CGFloat = 0.245
                 let offsetScale: CGFloat = 0.5
-                animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * offsetScale : -animationNodeFrame.width * offsetScale, dy: -25.0).insetBy(dx: -animationNodeFrame.width * scale, dy: -animationNodeFrame.height * scale)
+                animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * offsetScale : -animationNodeFrame.width * offsetScale, dy: -25.0)
             } else {
                 animationFrame = animationNodeFrame.insetBy(dx: -animationNodeFrame.width, dy: -animationNodeFrame.height)
                     .offsetBy(dx: incomingMessage ? animationNodeFrame.width - 10.0 : -animationNodeFrame.width + 10.0, dy: 0.0)
                 animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0))
             }
-                        
+            
             animationFrame = animationFrame.offsetBy(dx: 0.0, dy: self.insets.top)
             additionalAnimationNode.frame = animationFrame
             if incomingMessage {
diff --git a/submodules/TelegramUI/Components/LottieCpp/BUILD b/submodules/TelegramUI/Components/LottieCpp/BUILD
index 510c938526..50f747cca6 100644
--- a/submodules/TelegramUI/Components/LottieCpp/BUILD
+++ b/submodules/TelegramUI/Components/LottieCpp/BUILD
@@ -15,6 +15,7 @@ objc_library(
     copts = [
         "-Werror",
         "-I{}/Sources".format(package_name()),
+        "-O2",
     ],
     hdrs = glob([
         "PublicHeaders/**/*.h",
diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h
index 93a988af5b..e7112776ed 100644
--- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h
+++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h
@@ -12,6 +12,7 @@ extern "C" {
 @interface LottieAnimation : NSObject
 
 @property (nonatomic, readonly) NSInteger frameCount;
+@property (nonatomic, readonly) NSInteger framesPerSecond;
 @property (nonatomic, readonly) CGSize size;
 
 - (instancetype _Nullable)initWithData:(NSData * _Nonnull)data;
diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h
index d853b6bad5..e4158c3ccb 100644
--- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h
+++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h
@@ -42,15 +42,16 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, readonly, direct) CGFloat location;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct));
 
 @end
 
 @interface LottiePath : NSObject
 
-- (CGRect)boundingBox __attribute__((objc_direct));
 - (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct));
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct));
 
 @end
 
@@ -64,6 +65,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, readonly, direct) CGFloat opacity;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct));
 
 @end
 
@@ -76,6 +78,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, readonly, direct) CGPoint end;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct));
 
 @end
 
@@ -85,6 +88,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, readonly, direct) LottieFillRule fillRule;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct));
 
 @end
 
@@ -99,6 +103,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, strong, readonly, direct) NSArray<NSNumber *> * _Nullable dashPattern;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct));
 
 @end
 
@@ -109,6 +114,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct));
 
 @end
 
@@ -130,6 +136,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
 @property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask;
 
 - (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct));
 
 @end
 
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp
index 55c0c68225..8a3829e646 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp
@@ -43,8 +43,8 @@ public:
         std::shared_ptr<AssetLibrary> assetLibrary_,
         std::optional<std::vector<Marker>> markers_,
         std::optional<std::vector<FitzModifier>> fitzModifiers_,
-        std::optional<json11::Json> meta_,
-        std::optional<json11::Json> comps_
+        std::optional<lottiejson11::Json> meta_,
+        std::optional<lottiejson11::Json> comps_
     ) :
     startFrame(startFrame_),
     endFrame(endFrame_),
@@ -75,7 +75,7 @@ public:
     Animation(const Animation&) = delete;
     Animation& operator=(Animation&) = delete;
     
-    static std::shared_ptr<Animation> fromJson(json11::Json::object const &json) noexcept(false) {
+    static std::shared_ptr<Animation> fromJson(lottiejson11::Json::object const &json) noexcept(false) {
         auto name = getOptionalString(json, "nm");
         auto version = getString(json, "v");
         
@@ -168,14 +168,14 @@ public:
         );
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         if (name.has_value()) {
             result.insert(std::make_pair("nm", name.value()));
         }
         
-        result.insert(std::make_pair("v", json11::Json(version)));
+        result.insert(std::make_pair("v", lottiejson11::Json(version)));
         
         if (tgs.has_value()) {
             result.insert(std::make_pair("tgs", tgs.value()));
@@ -184,34 +184,34 @@ public:
         if (type.has_value()) {
             switch (type.value()) {
                 case CoordinateSpace::Type2d:
-                    result.insert(std::make_pair("ddd", json11::Json(0)));
+                    result.insert(std::make_pair("ddd", lottiejson11::Json(0)));
                     break;
                 case CoordinateSpace::Type3d:
-                    result.insert(std::make_pair("ddd", json11::Json(1)));
+                    result.insert(std::make_pair("ddd", lottiejson11::Json(1)));
                     break;
             }
         }
         
-        result.insert(std::make_pair("ip", json11::Json(startFrame)));
-        result.insert(std::make_pair("op", json11::Json(endFrame)));
-        result.insert(std::make_pair("fr", json11::Json(framerate)));
-        result.insert(std::make_pair("w", json11::Json(width)));
-        result.insert(std::make_pair("h", json11::Json(height)));
+        result.insert(std::make_pair("ip", lottiejson11::Json(startFrame)));
+        result.insert(std::make_pair("op", lottiejson11::Json(endFrame)));
+        result.insert(std::make_pair("fr", lottiejson11::Json(framerate)));
+        result.insert(std::make_pair("w", lottiejson11::Json(width)));
+        result.insert(std::make_pair("h", lottiejson11::Json(height)));
         
-        json11::Json::array layersArray;
+        lottiejson11::Json::array layersArray;
         for (const auto &layer : layers) {
-            json11::Json::object layerJson;
+            lottiejson11::Json::object layerJson;
             layer->toJson(layerJson);
             layersArray.push_back(layerJson);
         }
-        result.insert(std::make_pair("layers", json11::Json(layersArray)));
+        result.insert(std::make_pair("layers", lottiejson11::Json(layersArray)));
         
         if (glyphs.has_value()) {
-            json11::Json::array glyphArray;
+            lottiejson11::Json::array glyphArray;
             for (const auto &glyph : glyphs.value()) {
                 glyphArray.push_back(glyph->toJson());
             }
-            result.insert(std::make_pair("chars", json11::Json(glyphArray)));
+            result.insert(std::make_pair("chars", lottiejson11::Json(glyphArray)));
         }
         
         if (fonts.has_value()) {
@@ -223,19 +223,19 @@ public:
         }
         
         if (markers.has_value()) {
-            json11::Json::array markerArray;
+            lottiejson11::Json::array markerArray;
             for (const auto &marker : markers.value()) {
                 markerArray.push_back(marker.toJson());
             }
-            result.insert(std::make_pair("markers", json11::Json(markerArray)));
+            result.insert(std::make_pair("markers", lottiejson11::Json(markerArray)));
         }
         
         if (fitzModifiers.has_value()) {
-            json11::Json::array fitzModifierArray;
+            lottiejson11::Json::array fitzModifierArray;
             for (const auto &fitzModifier : fitzModifiers.value()) {
                 fitzModifierArray.push_back(fitzModifier.toJson());
             }
-            result.insert(std::make_pair("fitz", json11::Json(fitzModifierArray)));
+            result.insert(std::make_pair("fitz", lottiejson11::Json(fitzModifierArray)));
         }
         
         if (meta.has_value()) {
@@ -305,8 +305,8 @@ public:
     
     std::optional<std::vector<FitzModifier>> fitzModifiers;
     
-    std::optional<json11::Json> meta;
-    std::optional<json11::Json> comps;
+    std::optional<lottiejson11::Json> meta;
+    std::optional<lottiejson11::Json> comps;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp
index c071ab7854..dabec9756c 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp
@@ -14,7 +14,7 @@ public:
     id(id_) {
     }
     
-    explicit Asset(json11::Json::object const &json) noexcept(false) {
+    explicit Asset(lottiejson11::Json::object const &json) noexcept(false) {
         auto idData = getAny(json, "id");
         if (idData.is_string()) {
             id = idData.string_value();
@@ -30,7 +30,7 @@ public:
     Asset(const Asset&) = delete;
     Asset& operator=(Asset&) = delete;
     
-    virtual void toJson(json11::Json::object &json) const {
+    virtual void toJson(lottiejson11::Json::object &json) const {
         json.insert(std::make_pair("id", id));
         
         if (objectName.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp
index 7ff3b07451..5e7b893d02 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp
@@ -22,7 +22,7 @@ public:
     precompAssets(precompAssets_) {
     }
     
-    explicit AssetLibrary(json11::Json const &json) noexcept(false) {
+    explicit AssetLibrary(lottiejson11::Json const &json) noexcept(false) {
         if (!json.is_array()) {
             throw LottieParsingException();
         }
@@ -45,11 +45,11 @@ public:
         }
     }
     
-    json11::Json::array toJson() const {
-        json11::Json::array result;
+    lottiejson11::Json::array toJson() const {
+        lottiejson11::Json::array result;
         
         for (const auto &asset : assetList) {
-            json11::Json::object assetJson;
+            lottiejson11::Json::object assetJson;
             asset->toJson(assetJson);
             result.push_back(assetJson);
         }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp
index a1e63cbea7..f4210ea065 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp
@@ -23,7 +23,7 @@ public:
     
     virtual ~ImageAsset() = default;
     
-    explicit ImageAsset(json11::Json::object const &json) noexcept(false) :
+    explicit ImageAsset(lottiejson11::Json::object const &json) noexcept(false) :
     Asset(json) {
         name = getString(json, "p");
         directory = getString(json, "u");
@@ -34,7 +34,7 @@ public:
         _t = getOptionalString(json, "t");
     }
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         Asset::toJson(json);
         
         json.insert(std::make_pair("p", name));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp
index a6869c2081..d79f0016b8 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp
@@ -21,7 +21,7 @@ public:
     
     virtual ~PrecompAsset() = default;
     
-    explicit PrecompAsset(json11::Json::object const &json) noexcept(false) :
+    explicit PrecompAsset(lottiejson11::Json::object const &json) noexcept(false) :
     Asset(json) {
         frameRate = getOptionalDouble(json, "fr");
         
@@ -36,12 +36,12 @@ public:
         }
     }
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         Asset::toJson(json);
         
-        json11::Json::array layerArray;
+        lottiejson11::Json::array layerArray;
         for (const auto &layer : layers) {
-            json11::Json::object layerJson;
+            lottiejson11::Json::object layerJson;
             layer->toJson(layerJson);
             layerArray.push_back(layerJson);
         }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp
index ce591c7655..50549a1121 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp
@@ -27,7 +27,7 @@ public:
     isSingle(false) {
     }
     
-    KeyframeGroup(json11::Json::object const &json) noexcept(false) {
+    KeyframeGroup(lottiejson11::Json::object const &json) noexcept(false) {
         isAnimated = getOptionalInt(json, "a");
         expression = getOptionalAny(json, "x");
         expressionIndex = getOptionalInt(json, "ix");
@@ -101,15 +101,15 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         assert(!keyframes.empty());
         
         if (keyframes.size() == 1 && isSingle) {
             result.insert(std::make_pair("k", keyframes[0].value.toJson()));
         } else {
-            json11::Json::array containerData;
+            lottiejson11::Json::array containerData;
             
             for (const auto &keyframe : rawKeyframeData) {
                 containerData.push_back(keyframe.toJson());
@@ -138,7 +138,7 @@ public:
     std::vector<Keyframe<T>> keyframes;
     std::optional<int> isAnimated;
     
-    std::optional<json11::Json> expression;
+    std::optional<lottiejson11::Json> expression;
     std::optional<int> expressionIndex;
     std::vector<KeyframeData<T>> rawKeyframeData;
     bool isSingle = false;
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp
index 2787e4e557..23349f7390 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp
@@ -9,7 +9,7 @@ namespace lottie {
 /// A layer that holds an image.
 class ImageLayerModel: public LayerModel {
 public:
-    explicit ImageLayerModel(json11::Json::object const &json) noexcept(false) :
+    explicit ImageLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
     LayerModel(json) {
         referenceID = getString(json, "refId");
         
@@ -18,7 +18,7 @@ public:
     
     virtual ~ImageLayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         LayerModel::toJson(json);
         
         json.insert(std::make_pair("refId", referenceID));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp
index 6ceabadcf5..f14f1df9b8 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp
@@ -2,7 +2,7 @@
 
 namespace lottie {
 
-LayerType parseLayerType(json11::Json::object const &json, std::string const &key) {
+LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key) {
     if (const auto layerTypeValue = getOptionalInt(json, "ty")) {
         switch (layerTypeValue.value()) {
             case 0:
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp
index a5e08d4447..2670043c44 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp
@@ -21,7 +21,7 @@ enum class LayerType {
     Text
 };
 
-LayerType parseLayerType(json11::Json::object const &json, std::string const &key);
+LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key);
 int serializeLayerType(LayerType value);
 
 enum class MatteType: int {
@@ -53,7 +53,7 @@ enum class BlendMode: int {
 /// A base top container for shapes, images, and other view objects.
 class LayerModel {
 public:
-    explicit LayerModel(json11::Json::object const &json) noexcept(false) {
+    explicit LayerModel(lottiejson11::Json::object const &json) noexcept(false) {
         name = getOptionalString(json, "nm");
         index = getOptionalInt(json, "ind");
         
@@ -179,7 +179,7 @@ public:
     
     virtual ~LayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const {
+    virtual void toJson(lottiejson11::Json::object &json) const {
         if (name.has_value()) {
             json.insert(std::make_pair("nm", name.value()));
         }
@@ -219,7 +219,7 @@ public:
         }
         
         if (masks.has_value()) {
-            json11::Json::array maskArray;
+            lottiejson11::Json::array maskArray;
             for (const auto &mask : masks.value()) {
                 maskArray.push_back(mask->toJson());
             }
@@ -308,9 +308,9 @@ public:
     
     std::optional<bool> hasMask;
     std::optional<int> td;
-    std::optional<json11::Json> effectsData;
+    std::optional<lottiejson11::Json> effectsData;
     std::optional<std::string> layerClass;
-    std::optional<json11::Json> _extraHidden;
+    std::optional<lottiejson11::Json> _extraHidden;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp
index 6183d51be3..47bb5b1467 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp
@@ -8,7 +8,7 @@
 
 namespace lottie {
 
-std::shared_ptr<LayerModel> parseLayerModel(json11::Json::object const &json) noexcept(false) {
+std::shared_ptr<LayerModel> parseLayerModel(lottiejson11::Json::object const &json) noexcept(false) {
     LayerType layerType = parseLayerType(json, "ty");
     
     switch (layerType) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp
index 6f42ac527d..7b1b8cfe71 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp
@@ -7,7 +7,7 @@
 
 namespace lottie {
 
-std::shared_ptr<LayerModel> parseLayerModel(json11::Json::object const &json) noexcept(false);
+std::shared_ptr<LayerModel> parseLayerModel(lottiejson11::Json::object const &json) noexcept(false);
 
 }
 
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp
index 7f4956e803..7934474c78 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp
@@ -13,7 +13,7 @@ namespace lottie {
 /// A layer that holds another animation composition.
 class PreCompLayerModel: public LayerModel {
 public:
-    PreCompLayerModel(json11::Json::object const &json) :
+    PreCompLayerModel(lottiejson11::Json::object const &json) :
     LayerModel(json) {
         referenceID = getString(json, "refId");
         if (const auto timeRemappingData = getOptionalObject(json, "tm")) {
@@ -25,7 +25,7 @@ public:
     
     virtual ~PreCompLayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         LayerModel::toJson(json);
         
         json.insert(std::make_pair("refId", referenceID));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp
index 72477ef93b..61f4329bba 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp
@@ -12,7 +12,7 @@ namespace lottie {
 /// A layer that holds vector shape objects.
 class ShapeLayerModel: public LayerModel {
 public:
-    ShapeLayerModel(json11::Json::object const &json) noexcept(false) :
+    ShapeLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
     LayerModel(json) {
         auto shapeItemsData = getObjectArray(json, "shapes");
         for (const auto &shapeItemData : shapeItemsData) {
@@ -22,12 +22,12 @@ public:
     
     virtual ~ShapeLayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         LayerModel::toJson(json);
         
-        json11::Json::array shapeItemArray;
+        lottiejson11::Json::array shapeItemArray;
         for (const auto &item : items) {
-            json11::Json::object itemJson;
+            lottiejson11::Json::object itemJson;
             item->toJson(itemJson);
             shapeItemArray.push_back(itemJson);
         }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp
index 3a375d2a51..922d3e6342 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp
@@ -9,7 +9,7 @@ namespace lottie {
 /// A layer that holds a solid color.
 class SolidLayerModel: public LayerModel {
 public:
-    explicit SolidLayerModel(json11::Json::object const &json) noexcept(false) :
+    explicit SolidLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
     LayerModel(json) {
         colorHex = getString(json, "sc");
         width = getDouble(json, "sw");
@@ -18,7 +18,7 @@ public:
     
     virtual ~SolidLayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         LayerModel::toJson(json);
         
         json.insert(std::make_pair("sc", colorHex));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp
index 6ea5e62c28..37fb374ddf 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp
@@ -12,7 +12,7 @@ namespace lottie {
 /// A layer that holds text.
 class TextLayerModel: public LayerModel {
 public:
-    TextLayerModel(json11::Json::object const &json) :
+    TextLayerModel(lottiejson11::Json::object const &json) :
     LayerModel(json),
     text(KeyframeGroup<TextDocument>(TextDocument(
         "",
@@ -46,10 +46,10 @@ public:
     
     virtual ~TextLayerModel() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         LayerModel::toJson(json);
         
-        json11::Json::object textContainer;
+        lottiejson11::Json::object textContainer;
         textContainer.insert(std::make_pair("d", text.toJson()));
         if (_extraM.has_value()) {
             textContainer.insert(std::make_pair("m", _extraM.value()));
@@ -57,7 +57,7 @@ public:
         if (_extraP.has_value()) {
             textContainer.insert(std::make_pair("p", _extraP.value()));
         }
-        json11::Json::array animatorArray;
+        lottiejson11::Json::array animatorArray;
         for (const auto &animator : animators) {
             animatorArray.push_back(animator->toJson());
         }
@@ -73,9 +73,9 @@ public:
     /// Text animators
     std::vector<std::shared_ptr<TextAnimator>> animators;
     
-    std::optional<json11::Json> _extraM;
-    std::optional<json11::Json> _extraP;
-    std::optional<json11::Json> _extraA;
+    std::optional<lottiejson11::Json> _extraM;
+    std::optional<lottiejson11::Json> _extraP;
+    std::optional<lottiejson11::Json> _extraA;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp
index 6cd73101ff..87379ea722 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp
@@ -23,7 +23,7 @@ public:
     value(value_) {
     }
     
-    explicit DashElement(json11::Json::object const &json) noexcept(false) :
+    explicit DashElement(lottiejson11::Json::object const &json) noexcept(false) :
     type(DashElementType::Offset),
     value(KeyframeGroup<Vector1D>(Vector1D(0.0))) {
         auto typeRawValue = getString(json, "n");
@@ -42,8 +42,8 @@ public:
         name = getOptionalString(json, "nm");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         switch (type) {
             case DashElementType::Offset:
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp
index 163cf09c21..06553a99b6 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp
@@ -7,7 +7,7 @@ namespace lottie {
 
 class FitzModifier {
 public:
-    explicit FitzModifier(json11::Json::object const &json) noexcept(false) {
+    explicit FitzModifier(lottiejson11::Json::object const &json) noexcept(false) {
         original = getInt(json, "o");
         type12 = getOptionalInt(json, "f12");
         type3 = getOptionalInt(json, "f3");
@@ -16,8 +16,8 @@ public:
         type6 = getOptionalInt(json, "f6");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("o", (double)original));
         if (type12.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp
index 4219996a4b..4f715e9ac7 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp
@@ -18,14 +18,14 @@ public:
     frameTime(frameTime_) {
     }
     
-    explicit Marker(json11::Json::object const &json) noexcept(false) {
+    explicit Marker(lottiejson11::Json::object const &json) noexcept(false) {
         name = getString(json, "cm");
         frameTime = getDouble(json, "tm");
         dr = getOptionalInt(json, "dr");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("cm", name));
         result.insert(std::make_pair("tm", frameTime));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp
index d453a1c374..fd3c2cba7b 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp
@@ -19,7 +19,7 @@ enum class MaskMode {
 
 class Mask {
 public:
-    explicit Mask(json11::Json::object const &json) noexcept(false) :
+    explicit Mask(lottiejson11::Json::object const &json) noexcept(false) :
     opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
     shape(KeyframeGroup<BezierPath>(BezierPath())),
     inverted(false),
@@ -61,8 +61,8 @@ public:
         name = getOptionalString(json, "nm");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         if (_mode.has_value()) {
             switch (_mode.value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp
index d3bfa4471e..19cfc92da8 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp
@@ -43,7 +43,7 @@ public:
     _rotationZ(rotationZ_) {
     }
     
-    explicit Transform(json11::Json::object const &json) noexcept(false) {
+    explicit Transform(lottiejson11::Json::object const &json) noexcept(false) {
         // AnchorPoint
         if (const auto anchorPointDictionary = getOptionalObject(json, "a")) {
             _anchorPoint = KeyframeGroup<Vector3D>(anchorPointDictionary.value());
@@ -112,8 +112,8 @@ public:
         _extraSk = getOptionalAny(json, "sk");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         if (_anchorPoint.has_value()) {
             result.insert(std::make_pair("a", _anchorPoint->toJson()));
@@ -139,7 +139,7 @@ public:
                 result.insert(std::make_pair("p", _position->toJson()));
                 break;
             case PositionInternalRepresentation::NestedXY:
-                json11::Json::object nestedPosition;
+                lottiejson11::Json::object nestedPosition;
                 assert(_positionX.has_value());
                 assert(_positionY.has_value());
                 assert(!_position.has_value());
@@ -258,8 +258,8 @@ private:
     
     std::optional<bool> _extra_positionS;
     std::optional<std::string> _extraTy;
-    std::optional<json11::Json> _extraSa;
-    std::optional<json11::Json> _extraSk;
+    std::optional<lottiejson11::Json> _extraSa;
+    std::optional<lottiejson11::Json> _extraSk;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp
index 2565dbf817..17bd82d716 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp
@@ -17,7 +17,7 @@ enum class PathDirection: int {
 /// An item that define an ellipse shape
 class Ellipse: public ShapeItem {
 public:
-    explicit Ellipse(json11::Json::object const &json) noexcept(false) :
+    explicit Ellipse(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
     size(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))) {
@@ -43,7 +43,7 @@ public:
     
     virtual ~Ellipse() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (direction.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp
index 6666c3b238..194a08dcbb 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp
@@ -17,7 +17,7 @@ enum class FillRule: int {
 
 class Fill: public ShapeItem {
 public:
-    explicit Fill(json11::Json::object const &json) noexcept(false) :
+    explicit Fill(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     opacity(KeyframeGroup<Vector1D>(Vector1D(0.0))),
     color(KeyframeGroup<Color>(Color(0.0, 0.0, 0.0, 0.0))) {
@@ -45,7 +45,7 @@ public:
     
     virtual ~Fill() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("o", opacity.toJson()));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp
index 55c7efc9c2..fbe8bdbe16 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp
@@ -17,7 +17,7 @@ enum class GradientType: int {
 /// An item that define a gradient fill
 class GradientFill: public ShapeItem {
 public:
-    explicit GradientFill(json11::Json::object const &json) noexcept(false) :
+    explicit GradientFill(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
     startPoint(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@@ -60,7 +60,7 @@ public:
     
     virtual ~GradientFill() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("o", opacity.toJson()));
@@ -75,7 +75,7 @@ public:
             json.insert(std::make_pair("a", highlightAngle->toJson()));
         }
         
-        json11::Json::object colorsContainer;
+        lottiejson11::Json::object colorsContainer;
         colorsContainer.insert(std::make_pair("p", numberOfColors));
         colorsContainer.insert(std::make_pair("k", colors.toJson()));
         json.insert(std::make_pair("g", colorsContainer));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp
index 77b45b76aa..ff60fc1b20 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp
@@ -13,7 +13,7 @@ namespace lottie {
 /// An item that define a gradient stroke
 class GradientStroke: public ShapeItem {
 public:
-    explicit GradientStroke(json11::Json::object const &json) noexcept(false) :
+    explicit GradientStroke(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
     startPoint(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@@ -109,7 +109,7 @@ public:
     
     virtual ~GradientStroke() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("o", opacity.toJson()));
@@ -133,13 +133,13 @@ public:
             json.insert(std::make_pair("ml", miterLimit.value()));
         }
         
-        json11::Json::object colorsContainer;
+        lottiejson11::Json::object colorsContainer;
         colorsContainer.insert(std::make_pair("p", numberOfColors));
         colorsContainer.insert(std::make_pair("k", colors.toJson()));
         json.insert(std::make_pair("g", colorsContainer));
         
         if (dashPattern.has_value()) {
-            json11::Json::array dashElements;
+            lottiejson11::Json::array dashElements;
             for (const auto &dashElement : dashPattern.value()) {
                 dashElements.push_back(dashElement.toJson());
             }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp
index 10002a2691..7bc2eb4d10 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp
@@ -12,7 +12,7 @@ namespace lottie {
 /// An item that define an ellipse shape
 class Group: public ShapeItem {
 public:
-    explicit Group(json11::Json::object const &json) noexcept(false) :
+    explicit Group(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json) {
         auto itemsData = getObjectArray(json, "it");
         for (const auto &itemData : itemsData) {
@@ -24,12 +24,12 @@ public:
     
     virtual ~Group() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
-        json11::Json::array itemArray;
+        lottiejson11::Json::array itemArray;
         for (const auto &item : items) {
-            json11::Json::object itemJson;
+            lottiejson11::Json::object itemJson;
             item->toJson(itemJson);
             itemArray.push_back(itemJson);
         }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp
index 0492f66664..4f6fcc4881 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp
@@ -18,7 +18,7 @@ enum class MergeMode: int {
 /// An item that define an ellipse shape
 class Merge: public ShapeItem {
 public:
-    explicit Merge(json11::Json::object const &json) noexcept(false) :
+    explicit Merge(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     mode(MergeMode::None) {
         auto modeRawValue = getInt(json, "mm");
@@ -48,7 +48,7 @@ public:
     
     virtual ~Merge() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("mm", (int)mode));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp
index bb4a1d19e9..67926f2e29 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp
@@ -11,7 +11,7 @@ namespace lottie {
 /// An item that define an ellipse shape
 class Rectangle: public ShapeItem {
 public:
-    explicit Rectangle(json11::Json::object const &json) noexcept(false) :
+    explicit Rectangle(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
     size(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@@ -70,7 +70,7 @@ public:
     cornerRadius(cornerRadius_) {
     }
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (direction.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp
index 9c8b7f86b2..8bb25a1c37 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp
@@ -10,7 +10,7 @@ namespace lottie {
 /// An item that define a repeater
 class Repeater: public ShapeItem {
 public:
-    explicit Repeater(json11::Json::object const &json) noexcept(false) :
+    explicit Repeater(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json) {
         if (const auto copiesData = getOptionalObject(json, "c")) {
             copies = KeyframeGroup<Vector1D>(copiesData.value());
@@ -39,7 +39,7 @@ public:
     
     virtual ~Repeater() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (copies.has_value()) {
@@ -49,7 +49,7 @@ public:
             json.insert(std::make_pair("o", offset->toJson()));
         }
         
-        json11::Json::object transformContainer;
+        lottiejson11::Json::object transformContainer;
         if (startOpacity.has_value()) {
             json.insert(std::make_pair("so", startOpacity->toJson()));
         }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp
index f50a21e7c3..548d23a32d 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp
@@ -11,7 +11,7 @@ namespace lottie {
 /// An item that define an ellipse shape
 class RoundedRectangle: public ShapeItem {
 public:
-    explicit RoundedRectangle(json11::Json::object const &json) noexcept(false) :
+    explicit RoundedRectangle(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     cornerRadius(KeyframeGroup<Vector1D>(Vector1D(0.0))) {
         if (const auto directionRawValue = getOptionalInt(json, "d")) {
@@ -42,7 +42,7 @@ public:
     
     virtual ~RoundedRectangle() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (direction.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp
index b3b52ca370..0fd4a2dd76 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp
@@ -13,7 +13,7 @@ namespace lottie {
 /// An item that defines an custom shape
 class Shape: public ShapeItem {
 public:
-    explicit Shape(json11::Json::object const &json) noexcept(false) :
+    explicit Shape(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     path(KeyframeGroup<BezierPath>(getObject(json, "ks"))) {
         if (const auto directionRawValue = getOptionalInt(json, "d")) {
@@ -35,7 +35,7 @@ public:
     
     virtual ~Shape() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("ks", path.toJson()));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp
index ca859c56dd..a47b348c29 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp
@@ -17,7 +17,7 @@
 
 namespace lottie {
 
-std::shared_ptr<ShapeItem> parseShapeItem(json11::Json::object const &json) noexcept(false) {
+std::shared_ptr<ShapeItem> parseShapeItem(lottiejson11::Json::object const &json) noexcept(false) {
     auto typeRawValue = getString(json, "ty");
     if (typeRawValue == "el") {
         return std::make_shared<Ellipse>(json);
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp
index 2b05dba93a..892eba1db2 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp
@@ -27,13 +27,13 @@ enum class ShapeType {
 /// An item belonging to a Shape Layer
 class ShapeItem {
 public:
-    ShapeItem(json11::Json const &jsonAny) noexcept(false) :
+    ShapeItem(lottiejson11::Json const &jsonAny) noexcept(false) :
     type(ShapeType::Ellipse) {
         if (!jsonAny.is_object()) {
             throw LottieParsingException();
         }
         
-        json11::Json::object const &json = jsonAny.object_items();
+        lottiejson11::Json::object const &json = jsonAny.object_items();
         
         name = getOptionalString(json, "nm");
         matchName = getOptionalString(json, "mn");
@@ -106,7 +106,7 @@ public:
     ShapeItem(const ShapeItem&) = delete;
     ShapeItem& operator=(ShapeItem&) = delete;
     
-    virtual void toJson(json11::Json::object &json) const {
+    virtual void toJson(lottiejson11::Json::object &json) const {
         if (name.has_value()) {
             json.insert(std::make_pair("nm", name.value()));
         }
@@ -202,7 +202,7 @@ public:
     std::optional<std::string> layerClass;
 };
 
-std::shared_ptr<ShapeItem> parseShapeItem(json11::Json::object const &json) noexcept(false);
+std::shared_ptr<ShapeItem> parseShapeItem(lottiejson11::Json::object const &json) noexcept(false);
 
 }
 
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp
index 5160cf4a0b..fcfd28a2b8 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp
@@ -10,7 +10,7 @@ namespace lottie {
 /// An item that define a shape transform
 class ShapeTransform: public ShapeItem {
 public:
-    explicit ShapeTransform(json11::Json::object const &json) noexcept(false) :
+    explicit ShapeTransform(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json) {
         if (const auto anchorData = getOptionalObject(json, "a")) {
             anchor = KeyframeGroup<Vector3D>(anchorData.value());
@@ -37,7 +37,7 @@ public:
     
     virtual ~ShapeTransform() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (anchor.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp
index 7bc5fb16c6..c9f711526a 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp
@@ -19,7 +19,7 @@ enum class StarType: int {
 /// An item that define a star shape
 class Star: public ShapeItem {
 public:
-    explicit Star(json11::Json::object const &json) noexcept(false) :
+    explicit Star(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
     outerRadius(KeyframeGroup<Vector1D>(Vector1D(0.0))),
@@ -75,7 +75,7 @@ public:
     
     virtual ~Star() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         if (direction.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp
index 5930dbadd2..0131f7e69f 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp
@@ -15,7 +15,7 @@ namespace lottie {
 /// An item that define an ellipse shape
 class Stroke: public ShapeItem {
 public:
-    explicit Stroke(json11::Json::object const &json) noexcept(false) :
+    explicit Stroke(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
     color(KeyframeGroup<Color>(Color(0.0, 0.0, 0.0, 0.0))),
@@ -81,7 +81,7 @@ public:
     
     virtual ~Stroke() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("o", opacity.toJson()));
@@ -96,7 +96,7 @@ public:
         }
         
         if (dashPattern.has_value()) {
-            json11::Json::array dashElements;
+            lottiejson11::Json::array dashElements;
             for (const auto &dashElement : dashPattern.value()) {
                 dashElements.push_back(dashElement.toJson());
             }
@@ -134,7 +134,7 @@ public:
     std::optional<std::vector<DashElement>> dashPattern;
     
     std::optional<bool> fillEnabled;
-    std::optional<json11::Json> ml2;
+    std::optional<lottiejson11::Json> ml2;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp
index 7ae612262b..3e0365548b 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp
@@ -16,7 +16,7 @@ enum class TrimType: int {
 /// An item that defines trim
 class Trim: public ShapeItem {
 public:
-    explicit Trim(json11::Json::object const &json) noexcept(false) :
+    explicit Trim(lottiejson11::Json::object const &json) noexcept(false) :
     ShapeItem(json),
     start(KeyframeGroup<Vector1D>(Vector1D(0.0))),
     end(KeyframeGroup<Vector1D>(Vector1D(0.0))),
@@ -41,7 +41,7 @@ public:
     
     virtual ~Trim() = default;
     
-    virtual void toJson(json11::Json::object &json) const override {
+    virtual void toJson(lottiejson11::Json::object &json) const override {
         ShapeItem::toJson(json);
         
         json.insert(std::make_pair("s", start.toJson()));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp
index 8bde83023e..229a85c9dd 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp
@@ -22,7 +22,7 @@ public:
     ascent(ascent_) {
     }
     
-    explicit Font(json11::Json::object const &json) noexcept(false) {
+    explicit Font(lottiejson11::Json::object const &json) noexcept(false) {
         name = getString(json, "fName");
         familyName = getString(json, "fFamily");
         path = getOptionalString(json, "fPath");
@@ -33,8 +33,8 @@ public:
         origin = getOptionalInt(json, "origin");
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("fName", name));
         result.insert(std::make_pair("fFamily", familyName));
@@ -74,7 +74,7 @@ public:
     fonts(fonts_) {
     }
     
-    explicit FontList(json11::Json::object const &json) noexcept(false) {
+    explicit FontList(lottiejson11::Json::object const &json) noexcept(false) {
         if (const auto fontsData = getOptionalObjectArray(json, "list")) {
             for (const auto &fontData : fontsData.value()) {
                 fonts.emplace_back(fontData);
@@ -82,14 +82,14 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::array fontArray;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::array fontArray;
         
         for (const auto &font : fonts) {
             fontArray.push_back(font.toJson());
         }
         
-        json11::Json::object result;
+        lottiejson11::Json::object result;
         result.insert(std::make_pair("list", fontArray));
         return result;
     }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp
index 25d0af5fa9..17b09c0ce0 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp
@@ -27,7 +27,7 @@ public:
     shapes(shapes_) {
     }
     
-    explicit Glyph(json11::Json::object const &json) noexcept(false) :
+    explicit Glyph(lottiejson11::Json::object const &json) noexcept(false) :
     character(""),
     fontSize(0.0),
     fontFamily(""),
@@ -52,8 +52,8 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("ch", character));
         result.insert(std::make_pair("size", fontSize));
@@ -62,13 +62,13 @@ public:
         result.insert(std::make_pair("w", width));
         
         if (internalHasData || shapes.has_value()) {
-            json11::Json::object shapeContainer;
+            lottiejson11::Json::object shapeContainer;
             
             if (shapes.has_value()) {
-                json11::Json::array shapeArray;
+                lottiejson11::Json::array shapeArray;
                 
                 for (const auto &shape : shapes.value()) {
-                    json11::Json::object shapeJson;
+                    lottiejson11::Json::object shapeJson;
                     shape->toJson(shapeJson);
                     shapeArray.push_back(shapeJson);
                 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp
index e302a9051f..5004e74189 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp
@@ -41,18 +41,18 @@ public:
     tracking(tracking_) {
     }
     
-    explicit TextAnimator(json11::Json const &jsonAny) {
+    explicit TextAnimator(lottiejson11::Json const &jsonAny) {
         if (!jsonAny.is_object()) {
             throw LottieParsingException();
         }
-        json11::Json::object const &json = jsonAny.object_items();
+        lottiejson11::Json::object const &json = jsonAny.object_items();
         
         if (const auto nameData = getOptionalString(json, "nm")) {
             name = nameData.value();
         }
         _extraS = getOptionalAny(json, "s");
         
-        json11::Json::object const &animatorContainer = getObject(json, "a");
+        lottiejson11::Json::object const &animatorContainer = getObject(json, "a");
         
         if (const auto fillColorData = getOptionalObject(animatorContainer, "fc")) {
             fillColor = KeyframeGroup<Color>(fillColorData.value());
@@ -89,8 +89,8 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object animatorContainer;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object animatorContainer;
         
         if (fillColor.has_value()) {
             animatorContainer.insert(std::make_pair("fc", fillColor->toJson()));
@@ -126,7 +126,7 @@ public:
             animatorContainer.insert(std::make_pair("o", opacity->toJson()));
         }
         
-        json11::Json::object result;
+        lottiejson11::Json::object result;
         result.insert(std::make_pair("a", animatorContainer));
         
         if (name.has_value()) {
@@ -175,7 +175,7 @@ public:
     /// Tracking
     std::optional<KeyframeGroup<Vector1D>> tracking;
     
-    std::optional<json11::Json> _extraS;
+    std::optional<lottiejson11::Json> _extraS;
 };
 
 }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp
index 9eb3b1f154..72df5474b1 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp
@@ -48,7 +48,7 @@ public:
     textFrameSize(textFrameSize_) {
     }
     
-    explicit TextDocument(json11::Json const &jsonAny) noexcept(false) :
+    explicit TextDocument(lottiejson11::Json const &jsonAny) noexcept(false) :
     text(""),
     fontSize(0.0),
     fontFamily(""),
@@ -59,7 +59,7 @@ public:
             throw LottieParsingException();
         }
         
-        json11::Json::object const &json = jsonAny.object_items();
+        lottiejson11::Json::object const &json = jsonAny.object_items();
         
         text = getString(json, "t");
         fontSize = getDouble(json, "s");
@@ -103,8 +103,8 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("t", text));
         result.insert(std::make_pair("s", fontSize));
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp
index da1dcb51e6..f72968be59 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp
@@ -26,7 +26,7 @@ const char* LottieParsingException::what() const throw() {
     return "Lottie parsing exception";
 }
 
-json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false) {
+lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -34,7 +34,7 @@ json11::Json getAny(json11::Json::object const &object, std::string const &key)
     return value->second;
 }
 
-std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<lottiejson11::Json> getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -42,7 +42,7 @@ std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, s
     return value->second;
 }
 
-json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false) {
+lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -53,7 +53,7 @@ json11::Json::object getObject(json11::Json::object const &object, std::string c
     return value->second.object_items();
 }
 
-std::optional<json11::Json::object> getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<lottiejson11::Json::object> getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -64,7 +64,7 @@ std::optional<json11::Json::object> getOptionalObject(json11::Json::object const
     return value->second.object_items();
 }
 
-std::vector<json11::Json::object> getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::vector<lottiejson11::Json::object> getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -73,7 +73,7 @@ std::vector<json11::Json::object> getObjectArray(json11::Json::object const &obj
         throw LottieParsingException();
     }
     
-    std::vector<json11::Json::object> result;
+    std::vector<lottiejson11::Json::object> result;
     for (const auto &item : value->second.array_items()) {
         if (!item.is_object()) {
             throw LottieParsingException();
@@ -84,7 +84,7 @@ std::vector<json11::Json::object> getObjectArray(json11::Json::object const &obj
     return result;
 }
 
-std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<std::vector<lottiejson11::Json::object>> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -93,7 +93,7 @@ std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::
         throw LottieParsingException();
     }
     
-    std::vector<json11::Json::object> result;
+    std::vector<lottiejson11::Json::object> result;
     for (const auto &item : value->second.array_items()) {
         if (!item.is_object()) {
             throw LottieParsingException();
@@ -104,7 +104,7 @@ std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::
     return result;
 }
 
-std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::vector<lottiejson11::Json> getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -116,7 +116,7 @@ std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::s
     return value->second.array_items();
 }
 
-std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<std::vector<lottiejson11::Json>> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw std::nullopt;
@@ -128,7 +128,7 @@ std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::objec
     return value->second.array_items();
 }
 
-std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -139,7 +139,7 @@ std::string getString(json11::Json::object const &object, std::string const &key
     return value->second.string_value();
 }
 
-std::optional<std::string> getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<std::string> getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -150,7 +150,7 @@ std::optional<std::string> getOptionalString(json11::Json::object const &object,
     return value->second.string_value();
 }
 
-int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false) {
+int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -161,7 +161,7 @@ int32_t getInt(json11::Json::object const &object, std::string const &key) noexc
     return value->second.int_value();
 }
 
-std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<int32_t> getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -172,7 +172,7 @@ std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::s
     return value->second.int_value();
 }
 
-double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false) {
+double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -183,7 +183,7 @@ double getDouble(json11::Json::object const &object, std::string const &key) noe
     return value->second.number_value();
 }
 
-std::optional<double> getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<double> getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
@@ -194,7 +194,7 @@ std::optional<double> getOptionalDouble(json11::Json::object const &object, std:
     return value->second.number_value();
 }
 
-bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false) {
+bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         throw LottieParsingException();
@@ -205,7 +205,7 @@ bool getBool(json11::Json::object const &object, std::string const &key) noexcep
     return value->second.bool_value();
 }
 
-std::optional<bool> getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false) {
+std::optional<bool> getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
     auto value = object.find(key);
     if (value == object.end()) {
         return std::nullopt;
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp
index 37b666d38a..fc57ae101e 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp
@@ -23,29 +23,29 @@ public:
     virtual const char* what() const throw();
 };
 
-json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false);
+lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<lottiejson11::Json> getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<json11::Json::object> getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false);
+lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<lottiejson11::Json::object> getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-std::vector<json11::Json::object> getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false);
+std::vector<lottiejson11::Json::object> getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<std::vector<lottiejson11::Json::object>> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false);
+std::vector<lottiejson11::Json> getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<std::vector<lottiejson11::Json>> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<std::string> getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false);
+std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<std::string> getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false);
+int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<int32_t> getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<double> getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false);
+double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<double> getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
-bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false);
-std::optional<bool> getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false);
+bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
+std::optional<bool> getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
 
 }
 
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp
index 881a5917ef..66277737d8 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp
@@ -32,9 +32,9 @@ public:
     closed(false) {
     }
     
-    explicit BezierPathContents(json11::Json const &jsonAny) noexcept(false) :
+    explicit BezierPathContents(lottiejson11::Json const &jsonAny) noexcept(false) :
     elements({}) {
-        json11::Json::object const *json = nullptr;
+        lottiejson11::Json::object const *json = nullptr;
         if (jsonAny.is_object()) {
             json = &jsonAny.object_items();
         } else if (jsonAny.is_array()) {
@@ -97,12 +97,12 @@ public:
     BezierPathContents(const BezierPathContents&) = delete;
     BezierPathContents& operator=(BezierPathContents&) = delete;
     
-    json11::Json toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json toJson() const {
+        lottiejson11::Json::object result;
         
-        json11::Json::array vertices;
-        json11::Json::array inPoints;
-        json11::Json::array outPoints;
+        lottiejson11::Json::array vertices;
+        lottiejson11::Json::array inPoints;
+        lottiejson11::Json::array outPoints;
         
         for (const auto &element : elements) {
             vertices.push_back(element.vertex.point.toJson());
@@ -118,7 +118,7 @@ public:
             result.insert(std::make_pair("c", closed.value()));
         }
         
-        return json11::Json(result);
+        return lottiejson11::Json(result);
     }
     
     std::shared_ptr<CGPath> cgPath() const {
@@ -445,11 +445,11 @@ public:
     _contents(std::make_shared<BezierPathContents>()) {
     }
     
-    explicit BezierPath(json11::Json const &jsonAny) noexcept(false) :
+    explicit BezierPath(lottiejson11::Json const &jsonAny) noexcept(false) :
     _contents(std::make_shared<BezierPathContents>(jsonAny)) {
     }
     
-    json11::Json toJson() const {
+    lottiejson11::Json toJson() const {
         return _contents->toJson();
     }
     
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp
index 51f9ae4f13..1cf21a8667 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp
@@ -149,7 +149,7 @@ public:
     spatialOutTangent(spatialOutTangent_) {
     }
     
-    explicit KeyframeData(json11::Json const &json) noexcept(false) {
+    explicit KeyframeData(lottiejson11::Json const &json) noexcept(false) {
         if (!json.is_object()) {
             throw LottieParsingException();
         }
@@ -186,8 +186,8 @@ public:
         }
     }
     
-    json11::Json::object toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json::object toJson() const {
+        lottiejson11::Json::object result;
         
         if (startValue.has_value()) {
             result.insert(std::make_pair("s", startValue->toJson()));
@@ -240,7 +240,7 @@ public:
     /// The spacial out tangent of the vector.
     std::optional<Vector3D> spatialOutTangent;
     
-    std::optional<json11::Json> nData;
+    std::optional<lottiejson11::Json> nData;
     
     bool isHold() const {
         if (hold.has_value()) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.hpp
index 8fdbdea8ae..df436cc932 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.hpp
@@ -62,7 +62,7 @@ struct Color {
         a = a_ / denominatorValue;
     }
     
-    explicit Color(json11::Json const &jsonAny) noexcept(false) :
+    explicit Color(lottiejson11::Json const &jsonAny) noexcept(false) :
     r(0.0), g(0.0), b(0.0), a(0.0) {
         if (!jsonAny.is_array()) {
             throw LottieParsingException();
@@ -125,13 +125,13 @@ struct Color {
         a = a1;
     }
     
-    json11::Json toJson() const {
-        json11::Json::array result;
+    lottiejson11::Json toJson() const {
+        lottiejson11::Json::array result;
         
-        result.push_back(json11::Json(r));
-        result.push_back(json11::Json(g));
-        result.push_back(json11::Json(b));
-        result.push_back(json11::Json(a));
+        result.push_back(lottiejson11::Json(r));
+        result.push_back(lottiejson11::Json(g));
+        result.push_back(lottiejson11::Json(b));
+        result.push_back(lottiejson11::Json(a));
         
         return result;
     }
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp
index e1db01a15f..d37448999a 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp
@@ -11,7 +11,7 @@ struct GradientColorSet {
     GradientColorSet() {
     }
     
-    explicit GradientColorSet(json11::Json const &jsonAny) noexcept(false) {
+    explicit GradientColorSet(lottiejson11::Json const &jsonAny) noexcept(false) {
         if (!jsonAny.is_array()) {
             throw LottieParsingException();
         }
@@ -24,8 +24,8 @@ struct GradientColorSet {
         }
     }
     
-    json11::Json toJson() const {
-        json11::Json::array result;
+    lottiejson11::Json toJson() const {
+        lottiejson11::Json::array result;
         
         for (auto value : colors) {
             result.push_back(value);
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp
index 9cdff038d1..41eb1c5667 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp
@@ -18,7 +18,7 @@ struct Vector1D {
     value(value_) {
     }
     
-    explicit Vector1D(json11::Json const &json) noexcept(false) {
+    explicit Vector1D(lottiejson11::Json const &json) noexcept(false) {
         if (json.is_number()) {
             value = json.number_value();
         } else if (json.is_array()) {
@@ -34,8 +34,8 @@ struct Vector1D {
         }
     }
     
-    json11::Json toJson() const {
-            return json11::Json(value);
+    lottiejson11::Json toJson() const {
+            return lottiejson11::Json(value);
     }
     
     double value;
@@ -68,7 +68,7 @@ struct Vector2D {
     y(y_) {
     }
     
-    explicit Vector2D(json11::Json const &json) noexcept(false) {
+    explicit Vector2D(lottiejson11::Json const &json) noexcept(false) {
         x = 0.0;
         y = 0.0;
         
@@ -121,13 +121,13 @@ struct Vector2D {
         }
     }
     
-    json11::Json toJson() const {
-        json11::Json::object result;
+    lottiejson11::Json toJson() const {
+        lottiejson11::Json::object result;
         
         result.insert(std::make_pair("x", x));
         result.insert(std::make_pair("y", y));
         
-        return json11::Json(result);
+        return lottiejson11::Json(result);
     }
     
     double x;
@@ -200,7 +200,7 @@ struct Vector3D {
     z(z_) {
     }
     
-    explicit Vector3D(json11::Json const &json) noexcept(false) {
+    explicit Vector3D(lottiejson11::Json const &json) noexcept(false) {
         if (!json.is_array()) {
             throw LottieParsingException();
         }
@@ -236,14 +236,14 @@ struct Vector3D {
         }
     }
     
-    json11::Json toJson() const {
-        json11::Json::array result;
+    lottiejson11::Json toJson() const {
+        lottiejson11::Json::array result;
         
-        result.push_back(json11::Json(x));
-        result.push_back(json11::Json(y));
-        result.push_back(json11::Json(z));
+        result.push_back(lottiejson11::Json(x));
+        result.push_back(lottiejson11::Json(y));
+        result.push_back(lottiejson11::Json(z));
         
-        return json11::Json(result);
+        return lottiejson11::Json(result);
     }
     
     double x = 0.0;
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm
index 2d25dd7e0f..db68b68370 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm
@@ -17,7 +17,7 @@
     self = [super init];
     if (self != nil) {
         std::string errorText;
-        auto json = json11::Json::parse(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length), errorText);
+        auto json = lottiejson11::Json::parse(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length), errorText);
         if (!json.is_object()) {
             return nil;
         }
@@ -35,13 +35,17 @@
     return (NSInteger)(_animation->endFrame - _animation->startFrame);
 }
 
+- (NSInteger)framesPerSecond {
+    return (NSInteger)(_animation->framerate);
+}
+
 - (CGSize)size {
     return CGSizeMake(_animation->width, _animation->height);
 }
 
 - (NSData * _Nonnull)toJson {
-    json11::Json::object json = _animation->toJson();
-    std::string jsonString = json11::Json(json).dump();
+    lottiejson11::Json::object json = _animation->toJson();
+    std::string jsonString = lottiejson11::Json(json).dump();
     return [[NSData alloc] initWithBytes:jsonString.data() length:jsonString.size()];
 }
 
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm
index 9fe84fc5f3..db650d76ff 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm
@@ -321,13 +321,6 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
     Vector2D localTranslation(node->position().x + -node->bounds().x, node->position().y + -node->bounds().y);
     CATransform3D localTransform = node->transform();
     localTransform = localTransform.translated(localTranslation);
-    //if (localTransform.isIdentity()) {
-    //    localTransform.m41 += localTranslation.x;
-    //    localTransform.m42 += localTranslation.y;
-    //} else {
-    //    localTransform.m41 += localTranslation.x;
-    //    localTransform.m42 += localTranslation.y;
-    //}
     
     currentTransform = localTransform * currentTransform;
     
@@ -554,8 +547,6 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
         return nil;
     }
     
-    //processRenderTree(renderNode, lottie::Vector2D((int)size.width, (int)size.height), lottie::CATransform3D::identity().scaled(lottie::Vector2D(size.width / (double)_animation.size.width, size.height / (double)_animation.size.height)), false, *_bezierPathsBoundingBoxContext.get());
-    
     auto node = convertRenderTree(renderNode, lottie::Vector2D((int)size.width, (int)size.height), lottie::CATransform3D::identity().scaled(lottie::Vector2D(size.width / (double)_animation.size.width, size.height / (double)_animation.size.height)), false, *_bezierPathsBoundingBoxContext.get());
     
     if (node) {
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h
new file mode 100644
index 0000000000..e4158c3ccb
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h
@@ -0,0 +1,147 @@
+#ifndef LottieRenderTree_h
+#define LottieRenderTree_h
+
+#import <QuartzCore/QuartzCore.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef NS_ENUM(NSUInteger, LottiePathItemType) {
+    LottiePathItemTypeMoveTo,
+    LottiePathItemTypeLineTo,
+    LottiePathItemTypeCurveTo,
+    LottiePathItemTypeClose
+};
+
+typedef struct {
+    LottiePathItemType type;
+    CGPoint points[4];
+} LottiePathItem;
+
+typedef struct {
+    CGFloat r;
+    CGFloat g;
+    CGFloat b;
+    CGFloat a;
+} LottieColor;
+
+typedef NS_ENUM(NSUInteger, LottieFillRule) {
+    LottieFillRuleEvenOdd,
+    LottieFillRuleWinding
+};
+
+typedef NS_ENUM(NSUInteger, LottieGradientType) {
+    LottieGradientTypeLinear,
+    LottieGradientTypeRadial
+};
+
+@interface LottieColorStop : NSObject
+
+@property (nonatomic, readonly, direct) LottieColor color;
+@property (nonatomic, readonly, direct) CGFloat location;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct));
+
+@end
+
+@interface LottiePath : NSObject
+
+- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct));
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderContentShading : NSObject
+
+@end
+
+@interface LottieRenderContentSolidShading : LottieRenderContentShading
+
+@property (nonatomic, readonly, direct) LottieColor color;
+@property (nonatomic, readonly, direct) CGFloat opacity;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderContentGradientShading : LottieRenderContentShading
+
+@property (nonatomic, readonly, direct) CGFloat opacity;
+@property (nonatomic, readonly, direct) LottieGradientType gradientType;
+@property (nonatomic, strong, readonly, direct) NSArray<LottieColorStop *> * _Nonnull colorStops;
+@property (nonatomic, readonly, direct) CGPoint start;
+@property (nonatomic, readonly, direct) CGPoint end;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderContentFill : NSObject
+
+@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading;
+@property (nonatomic, readonly, direct) LottieFillRule fillRule;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderContentStroke : NSObject
+
+@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading;
+@property (nonatomic, readonly, direct) CGFloat lineWidth;
+@property (nonatomic, readonly, direct) CGLineJoin lineJoin;
+@property (nonatomic, readonly, direct) CGLineCap lineCap;
+@property (nonatomic, readonly, direct) CGFloat miterLimit;
+@property (nonatomic, readonly, direct) CGFloat dashPhase;
+@property (nonatomic, strong, readonly, direct) NSArray<NSNumber *> * _Nullable dashPattern;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderContent : NSObject
+
+@property (nonatomic, strong, readonly, direct) LottiePath * _Nonnull path;
+@property (nonatomic, strong, readonly, direct) LottieRenderContentStroke * _Nullable stroke;
+@property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct));
+
+@end
+
+@interface LottieRenderNode : NSObject
+
+@property (nonatomic, readonly, direct) CGPoint position;
+@property (nonatomic, readonly, direct) CGRect bounds;
+@property (nonatomic, readonly, direct) CATransform3D transform;
+@property (nonatomic, readonly, direct) CGFloat opacity;
+@property (nonatomic, readonly, direct) bool masksToBounds;
+@property (nonatomic, readonly, direct) bool isHidden;
+
+@property (nonatomic, readonly, direct) CGRect globalRect;
+@property (nonatomic, readonly, direct) CATransform3D globalTransform;
+@property (nonatomic, readonly, direct) LottieRenderContent * _Nullable renderContent;
+@property (nonatomic, readonly, direct) bool hasSimpleContents;
+@property (nonatomic, readonly, direct) bool isInvertedMatte;
+@property (nonatomic, readonly, direct) NSArray<LottieRenderNode *> * _Nonnull subnodes;
+@property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask;
+
+- (instancetype _Nonnull)init NS_UNAVAILABLE;
+- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct));
+
+@end
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LottieRenderTree_h */
diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm
index 19bfcb3f57..01890e9290 100644
--- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm
+++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm
@@ -1,4 +1,4 @@
-#include <LottieCpp/LottieRenderTree.h>
+#include "LottieRenderTree.h"
 #include "LottieRenderTreeInternal.h"
 
 #include "Lottie/Public/Primitives/CGPath.hpp"
@@ -15,6 +15,7 @@ namespace {
 
 @interface LottiePath () {
     std::vector<lottie::BezierPath> _paths;
+    NSData *_customData;
 }
 
 @end
@@ -29,83 +30,128 @@ namespace {
     return self;
 }
 
-/*- (instancetype _Nonnull)initWithCGPath:(CGPathRef _Nonnull)cgPath {
+- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct)) {
     self = [super init];
     if (self != nil) {
-        CGMutablePathRef mutableCopy = CGPathCreateMutableCopy(cgPath);
-        _path = std::make_shared<lottie::CGPathCocoaImpl>(mutableCopy);
-        CFRelease(mutableCopy);
+        _customData = customData;
     }
     return self;
-}*/
-
-- (CGRect)boundingBox {
-    lottie::CGRect result = bezierPathsBoundingBox(_paths);
-    return CGRectMake(result.x, result.y, result.width, result.height);
 }
 
 - (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate {
     LottiePathItem item;
     
-    for (const auto &path : _paths) {
-        std::optional<lottie::PathElement> previousElement;
-        for (const auto &element : path.elements()) {
-            if (previousElement.has_value()) {
-                if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) {
+    if (_customData != nil) {
+        int dataOffset = 0;
+        int dataLength = (int)_customData.length;
+        uint8_t const *dataBytes = (uint8_t const *)_customData.bytes;
+        while (dataOffset < dataLength) {
+            uint8_t itemType = dataBytes[dataOffset];
+            dataOffset += 1;
+            
+            switch (itemType) {
+                case 0: {
+                    Float32 px;
+                    memcpy(&px, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 py;
+                    memcpy(&py, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    item.type = LottiePathItemTypeMoveTo;
+                    item.points[0] = CGPointMake(px, py);
+                    iterate(&item);
+                    
+                    break;
+                }
+                case 1: {
+                    Float32 px;
+                    memcpy(&px, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 py;
+                    memcpy(&py, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
                     item.type = LottiePathItemTypeLineTo;
+                    item.points[0] = CGPointMake(px, py);
+                    iterate(&item);
+                    
+                    break;
+                }
+                case 2: {
+                    Float32 p1x;
+                    memcpy(&p1x, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 p1y;
+                    memcpy(&p1y, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 p2x;
+                    memcpy(&p2x, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 p2y;
+                    memcpy(&p2y, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 px;
+                    memcpy(&px, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    Float32 py;
+                    memcpy(&py, dataBytes + dataOffset, 4);
+                    dataOffset += 4;
+                    
+                    item.type = LottiePathItemTypeCurveTo;
+                    item.points[0] = CGPointMake(p1x, p1y);
+                    item.points[1] = CGPointMake(p2x, p2y);
+                    item.points[2] = CGPointMake(px, py);
+                    iterate(&item);
+                    
+                    break;
+                }
+                case 3: {
+                    item.type = LottiePathItemTypeClose;
+                    iterate(&item);
+                    break;
+                }
+                default: {
+                    break;
+                }
+            }
+        }
+    } else {
+        for (const auto &path : _paths) {
+            std::optional<lottie::PathElement> previousElement;
+            for (const auto &element : path.elements()) {
+                if (previousElement.has_value()) {
+                    if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) {
+                        item.type = LottiePathItemTypeLineTo;
+                        item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
+                        iterate(&item);
+                    } else {
+                        item.type = LottiePathItemTypeCurveTo;
+                        item.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
+                        item.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y);
+                        item.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y);
+                        iterate(&item);
+                    }
+                } else {
+                    item.type = LottiePathItemTypeMoveTo;
                     item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
                     iterate(&item);
-                } else {
-                    item.type = LottiePathItemTypeCurveTo;
-                    item.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
-                    item.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y);
-                    item.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y);
-                    iterate(&item);
                 }
-            } else {
-                item.type = LottiePathItemTypeMoveTo;
-                item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
-                iterate(&item);
+                previousElement = element;
             }
-            previousElement = element;
-        }
-        if (path.closed().value_or(true)) {
-            item.type = LottiePathItemTypeClose;
-            iterate(&item);
-        }
-    }
-    
-    /*_path->enumerate([iterate](lottie::CGPathItem const &element) {
-        LottiePathItem item;
-        
-        switch (element.type) {
-            case lottie::CGPathItem::Type::MoveTo: {
-                item.type = LottiePathItemTypeMoveTo;
-                item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
-                iterate(&item);
-                break;
-            }
-            case lottie::CGPathItem::Type::LineTo: {
-                item.type = LottiePathItemTypeLineTo;
-                item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
-                iterate(&item);
-                break;
-            }
-            case lottie::CGPathItem::Type::CurveTo: {
-                item.type = LottiePathItemTypeCurveTo;
-                item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
-                item.points[1] = CGPointMake(element.points[1].x, element.points[1].y);
-                item.points[2] = CGPointMake(element.points[2].x, element.points[2].y);
-                iterate(&item);
-                break;
-            }
-            case lottie::CGPathItem::Type::Close: {
+            if (path.closed().value_or(true)) {
                 item.type = LottiePathItemTypeClose;
                 iterate(&item);
-                break;
             }
         }
-    });*/
+    }
 }
 
 @end
@@ -155,6 +201,15 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
     return self;
 }
 
+- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity {
+    self = [super init];
+    if (self != nil) {
+        _color = color;
+        _opacity = opacity;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderContentGradientShading
@@ -187,6 +242,18 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
     return self;
 }
 
+- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct)) {
+    self = [super init];
+    if (self != nil) {
+        _opacity = opacity;
+        _gradientType = gradientType;
+        _colorStops = colorStops;
+        _start = start;
+        _end = end;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderContentFill
@@ -222,6 +289,15 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
     return self;
 }
 
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct)) {
+    self = [super init];
+    if (self != nil) {
+        _shading = shading;
+        _fillRule = fillRule;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderContentStroke
@@ -298,6 +374,20 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
     return self;
 }
 
+- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct)) {
+    self = [super init];
+    if (self != nil) {
+        _shading = shading;
+        _lineWidth = lineWidth;
+        _lineJoin = lineJoin;
+        _lineCap = lineCap;
+        _miterLimit = miterLimit;
+        _dashPhase = dashPhase;
+        _dashPattern = dashPattern;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderContent
@@ -316,10 +406,40 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
     return self;
 }
 
+- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct)) {
+    self = [super init];
+    if (self != nil) {
+        _path = path;
+        _stroke = stroke;
+        _fill = fill;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderNode
 
+- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct)) {
+    self = [super init];
+    if (self != nil) {
+        _position = position;
+        _bounds = bounds;
+        _transform = transform;
+        _opacity = opacity;
+        _masksToBounds = masksToBounds;
+        _isHidden = isHidden;
+        _globalRect = globalRect;
+        _globalTransform= globalTransform;
+        _renderContent = renderContent;
+        _hasSimpleContents = hasSimpleContents;
+        _isInvertedMatte = isInvertedMatte;
+        _subnodes = subnodes;
+        _mask = mask;
+    }
+    return self;
+}
+
 @end
 
 @implementation LottieRenderNode (Internal)
diff --git a/submodules/TelegramUI/Components/LottieMetal/BUILD b/submodules/TelegramUI/Components/LottieMetal/BUILD
index 9c92883589..dd63b1b0eb 100644
--- a/submodules/TelegramUI/Components/LottieMetal/BUILD
+++ b/submodules/TelegramUI/Components/LottieMetal/BUILD
@@ -1,5 +1,46 @@
 load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
 
+load(
+    "@build_bazel_rules_apple//apple:resources.bzl",
+    "apple_resource_bundle",
+    "apple_resource_group",
+)
+load("//build-system/bazel-utils:plist_fragment.bzl",
+    "plist_fragment",
+)
+
+filegroup(
+    name = "LottieMetalSources",
+    srcs = glob([
+        "Metal/**/*.metal",
+    ]),
+    visibility = ["//visibility:public"],
+)
+
+plist_fragment(
+    name = "LottieMetalSourcesBundleInfoPlist",
+    extension = "plist",
+    template =
+    """
+    <key>CFBundleIdentifier</key>
+    <string>org.telegram.LottieMetalSources</string>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>en</string>
+    <key>CFBundleName</key>
+    <string>LottieMetal</string>
+    """
+)
+
+apple_resource_bundle(
+    name = "LottieMetalSourcesBundle",
+    infoplists = [
+        ":LottieMetalSourcesBundleInfoPlist",
+    ],
+    resources = [
+        ":LottieMetalSources",
+    ],
+)
+
 swift_library(
     name = "LottieMetal",
     module_name = "LottieMetal",
@@ -9,6 +50,9 @@ swift_library(
     copts = [
         "-warnings-as-errors",
     ],
+    data = [
+        ":LottieMetalSourcesBundle",
+    ],
     deps = [
         "//submodules/SSignalKit/SwiftSignalKit",
         "//submodules/AsyncDisplayKit",
@@ -17,6 +61,7 @@ swift_library(
         "//submodules/GZip",
         "//submodules/MetalEngine",
         "//submodules/TelegramUI/Components/LottieCpp",
+        "//submodules/Components/HierarchyTrackingLayer",
     ],
     visibility = [
         "//visibility:public",
diff --git a/submodules/TelegramUI/Components/LottieMetal/Metal/LottieMetalShaders.metal b/submodules/TelegramUI/Components/LottieMetal/Metal/LottieMetalShaders.metal
new file mode 100644
index 0000000000..62257c7dae
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Metal/LottieMetalShaders.metal
@@ -0,0 +1,872 @@
+#include <metal_stdlib>
+using namespace metal;
+
+typedef struct
+{
+    packed_float2 position;
+    packed_float2 texCoord;
+} QuadVertex;
+
+typedef struct
+{
+    packed_float2 position;
+} Vertex;
+
+typedef struct
+{
+    float4 position [[position]];
+    float2 texCoord;
+    float2 transformedPosition;
+} QuadOut;
+
+typedef struct
+{
+    float4 position [[position]];
+    float direction;
+} FillVertexOut;
+
+float calculateNormalDirection(float2 a, float2 b, float2 c) {
+    float2 ab = b - a;
+    float2 ac = c - a;
+    
+    return ab.x * ac.y - ab.y * ac.x;
+}
+
+vertex QuadOut quad_vertex_shader(
+    device QuadVertex const *vertices [[buffer(0)]],
+    uint vertexId [[vertex_id]],
+    device matrix<float, 4> const &transform [[buffer(1)]]
+) {
+    QuadVertex in = vertices[vertexId];
+    QuadOut out;
+    float4 position = transform * float4(float2(in.position), 0.0, 1.0);
+    out.position = position;
+    out.texCoord = in.texCoord;
+    out.transformedPosition = (transform * float4(float2(in.position), 0.0, 1.0)).xy;
+
+    return out;
+}
+
+vertex FillVertexOut fill_vertex_shader(
+    device Vertex const *vertices [[buffer(0)]],
+    uint vertexId [[vertex_id]],
+    device matrix<float, 4> const &transform [[buffer(1)]],
+    device packed_float2 const &baseVertex [[buffer(2)]]
+) {
+    FillVertexOut out;
+    uint triangleIndex = vertexId / 3;
+    uint vertexInTriangleIndex = vertexId % 3;
+    
+    //[0, 1], [1, 2], [2, 3]...
+    //0,      1,      2
+    
+    float2 sourcePosition;
+    float2 v1 = float2(vertices[triangleIndex].position);
+    float2 v2 = float2(vertices[triangleIndex + 1].position);
+    
+    sourcePosition = select(
+        select(
+            v2,
+            v1,
+            vertexInTriangleIndex == 1
+        ),
+        baseVertex,
+        vertexInTriangleIndex == 0
+    );
+    
+    float normalDirection = calculateNormalDirection(baseVertex, v1, v2);
+    
+    float4 position = transform * float4(sourcePosition, 0.0, 1.0);
+    out.position = position;
+    
+    out.direction = sign(normalDirection);
+
+    return out;
+}
+
+struct ShapeOut {
+    half4 color [[color(1)]];
+};
+
+fragment ShapeOut fragment_shader(
+    FillVertexOut in [[stage_in]],
+    ShapeOut current,
+    device const int32_t &mode [[buffer(1)]]
+) {
+    ShapeOut out = current;
+    
+    if (mode == 0) {
+        half result = select(out.color.r, half(127.0 / 255.0), out.color.r == 0.0);
+        result += half(in.direction) * 3.0 / 255.0;
+        out.color.r = result;
+    } else {
+        out.color.r = out.color.r == 0.0 ? 1.0 : 0.0;
+    }
+    return out;
+}
+
+fragment ShapeOut clear_mask_fragment(
+    QuadOut in [[stage_in]]
+) {
+    ShapeOut out;
+    out.color = half4(0.0);
+    return out;
+}
+
+struct ColorOut {
+    half4 color [[color(0)]];
+};
+
+fragment ColorOut merge_color_fill_fragment_shader(
+    ShapeOut colorIn,
+    device const float4 &color [[buffer(0)]],
+    device const int32_t &mode [[buffer(1)]]
+) {
+    ColorOut out;
+    
+    half4 sampledColor = half4(color);
+    sampledColor.r = sampledColor.r * sampledColor.a;
+    sampledColor.g = sampledColor.g * sampledColor.a;
+    sampledColor.b = sampledColor.b * sampledColor.a;
+    
+    if (mode == 0) {
+        half diff = abs(colorIn.color.r - 127.0 / 255.0);
+        float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
+        float outColorFactor = select(
+            0.0,
+            diffSelect,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        out.color = sampledColor * outColorFactor;
+    } else {
+        float outColorFactor = select(
+            0.0,
+            1.0,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        
+        out.color = sampledColor * outColorFactor;
+    }
+    
+    if (out.color.a == 0.0) {
+        //discard_fragment();
+    }
+
+    return out;
+}
+
+typedef struct
+{
+    packed_float4 color;
+    float location;
+} GradientColorStop;
+
+float linearGradientStep(float edge0, float edge1, float x) {
+    float t = clamp((x - edge0) / (edge1 - edge0), float(0), float(1));
+    return t;
+}
+
+fragment ColorOut merge_linear_gradient_fill_fragment_shader(
+    QuadOut quadIn [[stage_in]],
+    ShapeOut colorIn,
+    device const GradientColorStop *colorStops [[buffer(0)]],
+    device const int32_t &mode [[buffer(1)]],
+    device const uint &numColorStops [[buffer(2)]],
+    device const packed_float2 &localStartPosition [[buffer(3)]],
+    device const packed_float2 &localEndPosition [[buffer(4)]]
+) {
+    ColorOut out;
+    
+    float4 sourceColor;
+    
+    if (numColorStops <= 1) {
+        sourceColor = colorStops[0].color;
+    } else {
+        float2 localPixelPosition = quadIn.transformedPosition.xy;
+        
+        float2 gradientVector = normalize(localEndPosition - localStartPosition);
+        float2 pointVector = localPixelPosition - localStartPosition;
+        float pixelDistance = dot(pointVector, gradientVector) / dot(gradientVector, gradientVector);
+        float gradientLength = length(localEndPosition - localStartPosition);
+        float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
+        
+        sourceColor = mix(colorStops[0].color, colorStops[1].color, linearGradientStep(
+            colorStops[0].location,
+            colorStops[1].location,
+            pixelValue
+        ));
+        for (int i = 1; i < (int)numColorStops - 1; i++) {
+            sourceColor = mix(sourceColor, colorStops[i + 1].color, linearGradientStep(
+                colorStops[i].location,
+                colorStops[i + 1].location,
+                pixelValue
+            ));
+        }
+    }
+    
+    half4 sampledColor = half4(sourceColor);
+    
+    sampledColor.r = sampledColor.r * sampledColor.a;
+    sampledColor.g = sampledColor.g * sampledColor.a;
+    sampledColor.b = sampledColor.b * sampledColor.a;
+    
+    if (mode == 0) {
+        half diff = abs(colorIn.color.r - 127.0 / 255.0);
+        float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
+        float outColorFactor = select(
+            0.0,
+            diffSelect,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        out.color = sampledColor * outColorFactor;
+    } else {
+        float outColorFactor = select(
+            0.0,
+            1.0,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        
+        out.color = sampledColor * outColorFactor;
+    }
+    
+    if (out.color.a == 0.0) {
+        //discard_fragment();
+    }
+
+    return out;
+}
+
+fragment ColorOut merge_radial_gradient_fill_fragment_shader(
+    QuadOut quadIn [[stage_in]],
+    ShapeOut colorIn,
+    device const GradientColorStop *colorStops [[buffer(0)]],
+    device const int32_t &mode [[buffer(1)]],
+    device const uint &numColorStops [[buffer(2)]],
+    device const packed_float2 &localStartPosition [[buffer(3)]],
+    device const packed_float2 &localEndPosition [[buffer(4)]]
+) {
+    ColorOut out;
+    
+    float4 sourceColor;
+    
+    if (numColorStops <= 1) {
+        sourceColor = colorStops[0].color;
+    } else {
+        float pixelDistance = distance(quadIn.transformedPosition.xy, localStartPosition);
+        float gradientLength = length(localEndPosition - localStartPosition);
+        float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
+        
+        sourceColor = colorStops[0].color;
+        for (int i = 0; i < (int)numColorStops - 1; i++) {
+            float currentStopLocation = colorStops[i].location;
+            float nextStopLocation = colorStops[i + 1].location;
+            float4 nextStopColor = colorStops[i + 1].color;
+            sourceColor = mix(sourceColor, nextStopColor, linearGradientStep(
+                currentStopLocation,
+                nextStopLocation,
+                pixelValue
+            ));
+        }
+    }
+    
+    half4 sampledColor = half4(sourceColor);
+    
+    sampledColor.r = sampledColor.r * sampledColor.a;
+    sampledColor.g = sampledColor.g * sampledColor.a;
+    sampledColor.b = sampledColor.b * sampledColor.a;
+    
+    if (mode == 0) {
+        half diff = abs(colorIn.color.r - 127.0 / 255.0);
+        float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
+        float outColorFactor = select(
+            0.0,
+            diffSelect,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        out.color = sampledColor * outColorFactor;
+    } else {
+        float outColorFactor = select(
+            0.0,
+            1.0,
+            colorIn.color.r > 1.0 / 255.0
+        );
+        
+        out.color = sampledColor * outColorFactor;
+    }
+    
+    if (out.color.a == 0.0) {
+        //discard_fragment();
+    }
+
+    return out;
+}
+
+typedef struct {
+    packed_float2 position;
+} StrokePositionIn;
+
+typedef struct {
+    packed_float2 point;
+} StrokePointIn;
+
+typedef struct {
+    float id;
+} StrokeRoundJoinVertexIn;
+
+typedef struct {
+    packed_float4 position;
+} StrokeMiterJoinVertexIn;
+
+typedef struct {
+    packed_float3 position;
+} StrokeBevelJoinVertexIn;
+
+typedef struct {
+    packed_float2 position;
+} StrokeCapVertexIn;
+
+typedef struct
+{
+    float4 position [[position]];
+} StrokeVertexOut;
+
+fragment ColorOut stroke_fragment_shader(
+    StrokeVertexOut in [[stage_in]],
+    ShapeOut colorIn,
+    device const float4 &color [[buffer(0)]]
+) {
+    ColorOut out;
+    
+    half4 result = half4(color);
+    result.r *= result.a;
+    result.g *= result.a;
+    result.b *= result.a;
+    
+    out.color = result;
+
+    return out;
+}
+
+typedef struct {
+    int32_t bufferOffset; // 4
+    packed_float2 start; // 4 * 2
+    packed_float2 end; // 4 * 2
+    packed_float2 cp1; // 4 * 2
+    packed_float2 cp2; // 4 * 2
+    float offset; // 4
+} BezierInputItem;
+
+kernel void evaluateBezier(
+    device BezierInputItem const *inputItems [[buffer(0)]],
+    device float *vertexData [[buffer(1)]],
+    device uint const &itemCount [[buffer(2)]],
+    uint2 index [[ thread_position_in_grid ]]
+) {
+    if (index.x >= itemCount) {
+        return;
+    }
+    BezierInputItem item = inputItems[index.x];
+    
+    float2 p0 = item.start;
+    float2 p1 = item.cp1;
+    float2 p2 = item.cp2;
+    float2 p3 = item.end;
+    
+    float t = (((float)index.y) + 1.0) / (8.0);
+    float oneMinusT = 1.0 - t;
+    
+    float2 value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
+    
+    vertexData[item.bufferOffset + 2 * index.y] = value.x;
+    vertexData[item.bufferOffset + 2 * index.y + 1] = value.y;
+}
+
+fragment half4 quad_offscreen_fragment(
+    QuadOut in [[stage_in]],
+    texture2d<half, access::sample> texture[[texture(0)]],
+    device float const &opacity [[buffer(1)]]
+) {
+    constexpr sampler s(address::clamp_to_edge, filter::linear);
+    half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
+    
+    color *= half(opacity);
+    
+    return color;
+}
+
+fragment half4 quad_offscreen_fragment_with_mask(
+    QuadOut in [[stage_in]],
+    texture2d<half, access::sample> texture[[texture(0)]],
+    texture2d<half, access::sample> maskTexture[[texture(1)]],
+    device float const &opacity [[buffer(1)]],
+    device uint const &maskMode [[buffer(2)]]
+) {
+    constexpr sampler s(address::clamp_to_edge, filter::linear);
+    half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
+    half4 maskColor = maskTexture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
+    
+    if (maskMode == 0) {
+        color *= maskColor.a;
+    } else {
+        color *= 1.0 - maskColor.a;
+    }
+    
+    color *= half(opacity);
+    
+    return color;
+}
+
+bool myIsNan(float val) {
+    return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;
+}
+
+bool isLinePointInvalid(float4 p) {
+  return p.w == 0.0 || myIsNan(p.x);
+}
+
+// Adapted from https://github.com/rreusser/regl-gpu-lines
+
+vertex StrokeVertexOut strokeTerminalVertex(
+    uint instanceId [[instance_id]],
+    uint index [[vertex_id]],
+    device StrokePointIn const *points [[buffer(0)]],
+    device matrix<float, 4> const &transform [[buffer(1)]],
+    device packed_float2 const &_vertCnt2 [[buffer(2)]],
+    device packed_float2 const &_capJoinRes2 [[buffer(3)]],
+    device uint const &isJoinRound [[buffer(4)]],
+    device uint const &isCapRound [[buffer(5)]],
+    device float const &miterLimit [[buffer(6)]],
+    device float const &width [[buffer(7)]]
+) {
+    const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
+    const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
+    
+    float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
+    
+    const float pi = 3.141592653589793;
+    
+    float2 xyB = points[instanceId * 3 + 0].point;
+    float2 xyC = points[instanceId * 3 + 1].point;
+    float2 xyD = points[instanceId * 3 + 2].point;
+    
+    StrokeVertexOut out;
+
+    float4 pB = float4(xyB, 0.0, 1.0);
+    float4 pC = float4(xyC, 0.0, 1.0);
+    float4 pD = float4(xyD, 0.0, 1.0);
+
+    // A sensible default for early returns
+    out.position = pB;
+
+    bool aInvalid = false;
+    bool bInvalid = isLinePointInvalid(pB);
+    bool cInvalid = isLinePointInvalid(pC);
+    bool dInvalid = isLinePointInvalid(pD);
+
+    // Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
+    // these vertices may be used, for example if we have enough for a round cap but only draw a miter
+    // join.
+    float2 v = _vertCnt2 + 3.0;
+
+    // Total vertex count
+    float N = dot(v, float2(1));
+
+    // If we're past the first half-join and half of the segment, then we swap all vertices and start
+    // over from the opposite end.
+    bool mirror = index >= v.x;
+
+    // When rendering dedicated endpoints, this allows us to insert an end cap *alone* (without the attached
+    // segment and join)
+    if (dInvalid && mirror) {
+        return out;
+    }
+
+    // Convert to screen-pixel coordinates
+    // Save w so we can perspective re-multiply at the end to get varyings depth-correct
+    float pw = mirror ? pC.w : pB.w;
+    pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
+    pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
+    pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
+
+    // If it's a cap, mirror A back onto C to accomplish a round
+    float4 pA = pC;
+
+    // Reject if invalid or if outside viewing planes
+    if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
+        return out;
+    }
+
+    // Swap everything computed so far if computing mirrored half
+    if (mirror) {
+        float4 vTmp = pC; pC = pB; pB = vTmp;
+        vTmp = pD; pD = pA; pA = vTmp;
+        bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
+    }
+
+    bool isCap = !mirror;
+
+    // Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
+    // degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
+    if (aInvalid) { pA = 2.0 * pB - pC; }
+    if (dInvalid) { pD = 2.0 * pC - pB; }
+    bool roundOrCap = isJoinRound || isCap;
+
+    // Tangent and normal vectors
+    float2 tBC = pC.xy - pB.xy;
+    float lBC = length(tBC);
+    tBC /= lBC;
+    float2 nBC = float2(-tBC.y, tBC.x);
+
+    float2 tAB = pB.xy - pA.xy;
+    float lAB = length(tAB);
+    if (lAB > 0.0) tAB /= lAB;
+    float2 nAB = float2(-tAB.y, tAB.x);
+
+    float2 tCD = pD.xy - pC.xy;
+    float lCD = length(tCD);
+    if (lCD > 0.0) tCD /= lCD;
+    float2 nCD = float2(-tCD.y, tCD.x);
+
+    // Clamp for safety, since we take the arccos
+    float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
+
+    // This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
+    // of the middle segment. The fix appears straightforward, but this took a few hours to get right.
+    const float tol = 1e-4;
+    float mirrorSign = mirror ? -1.0 : 1.0;
+    float dirB = -dot(tBC, nAB);
+    float dirC = dot(tBC, nCD);
+    bool bCollinear = abs(dirB) < tol;
+    bool cCollinear = abs(dirC) < tol;
+    bool bIsHairpin = bCollinear && cosB < 0.0;
+    // bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
+    dirB = bCollinear ? -mirrorSign : sign(dirB);
+    dirC = cCollinear ? -mirrorSign : sign(dirC);
+
+    float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
+
+    // Compute our primary "join index", that is, the index starting at the very first point of the join.
+    // The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
+    float i = mirror ? N - index : index;
+
+    // Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
+    // that's the only form in which we use this number.
+    float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
+
+    // Shift the index to send unused vertices to an index below zero, which will then just get clamped to
+    // zero and result in repeated points, i.e. degenerate triangles.
+    i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
+
+    // Use the direction to offset the index by one. This has the effect of flipping the winding number so
+    // that it's always consistent no matter which direction the join turns.
+    i += (dirB < 0.0 ? -1.0 : 0.0);
+
+    // Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
+    // in the middle, where the mirrored and unmirrored halves meet.
+    i -= mirror ? 1.0 : 0.0;
+
+    // Clamp to zero and repeat unused excess vertices.
+    i = max(0.0, i);
+
+    // Start with a default basis pointing along the segment with normal vector outward
+    float2 xBasis = tBC;
+    float2 yBasis = nBC * dirB;
+
+    // Default point is 0 along the segment, 1 (width unit) normal to it
+    float2 xy = float2(0);
+
+    if (i == res + 1.0) {
+        // pick off this one specific index to be the interior miter point
+        // If not div-by-zero, then sinB / (1 + cosB)
+        float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
+        xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
+    } else {
+        // Draw half of a join
+        float m2 = dot(miter, miter);
+        float lm = sqrt(m2);
+        yBasis = miter / lm;
+        xBasis = dirB * float2(yBasis.y, -yBasis.x);
+        bool isBevel = 1.0 > miterLimit * m2;
+        
+        if (((int)i) % 2 == 0) {
+            // Outer joint points
+            if (roundOrCap || i != 0.0) {
+                // Round joins
+                float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
+                xy = float2(cos(theta), sin(theta));
+                
+                if (isCap) {
+                    // A special multiplier factor for turning 3-point rounds into square caps (but leave the
+                    // y == 0.0 point unaffected)
+                    if (xy.y > 0.001) xy *= _capScale;
+                }
+            } else {
+                // Miter joins
+                yBasis = bIsHairpin ? float2(0) : miter;
+                xy.y = isBevel ? 1.0 : 1.0 / m2;
+            }
+        } else {
+            // Offset the center vertex position to get bevel SDF correct
+            if (isBevel && !roundOrCap) {
+                xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
+            }
+        }
+    }
+
+    // Point offset from main vertex position
+    float2 dP = float2x2(xBasis, yBasis) * xy;
+
+    out.position = pB;
+    out.position.xy += width * dP;
+    out.position *= pw;
+    out.position = transform * out.position;
+    
+    return out;
+}
+
+vertex StrokeVertexOut strokeInnerVertex(
+    uint instanceId [[instance_id]],
+    uint index [[vertex_id]],
+    device StrokePointIn const *points [[buffer(0)]],
+    device matrix<float, 4> const &transform [[buffer(1)]],
+    device packed_float2 const &_vertCnt2 [[buffer(2)]],
+    device packed_float2 const &_capJoinRes2 [[buffer(3)]],
+    device uint const &isJoinRound [[buffer(4)]],
+    device uint const &isCapRound [[buffer(5)]],
+    device float const &miterLimit [[buffer(6)]],
+    device float const &width [[buffer(7)]]
+) {
+    const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
+    const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
+    
+    float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
+    
+    const float pi = 3.141592653589793;
+    
+    float2 xyA = points[instanceId + 0].point;
+    float2 xyB = points[instanceId + 1].point;
+    float2 xyC = points[instanceId + 2].point;
+    float2 xyD = points[instanceId + 3].point;
+    
+    StrokeVertexOut out;
+    
+    float4 pA = float4(xyA, 0.0, 1.0);
+    float4 pB = float4(xyB, 0.0, 1.0);
+    float4 pC = float4(xyC, 0.0, 1.0);
+    float4 pD = float4(xyD, 0.0, 1.0);
+    
+    // A sensible default for early returns
+    out.position = pB;
+    
+    bool aInvalid = isLinePointInvalid(pA);
+    bool bInvalid = isLinePointInvalid(pB);
+    bool cInvalid = isLinePointInvalid(pC);
+    bool dInvalid = isLinePointInvalid(pD);
+    
+    // Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
+    // these vertices may be used, for example if we have enough for a round cap but only draw a miter
+    // join.
+    float2 v = _vertCnt2 + 3.0;
+    
+    // Total vertex count
+    float N = dot(v, float2(1));
+    
+    // If we're past the first half-join and half of the segment, then we swap all vertices and start
+    // over from the opposite end.
+    bool mirror = index >= v.x;
+    
+    // When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached
+    // segment and join)
+    
+    
+    // Convert to screen-pixel coordinates
+    // Save w so we can perspective re-multiply at the end to get varyings depth-correct
+    float pw = mirror ? pC.w : pB.w;
+    pA = float4(float3(pA.xy, pA.z) / pA.w, 1);
+    pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
+    pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
+    pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
+    
+    // If it's a cap, mirror A back onto C to accomplish a round
+    
+    
+    // Reject if invalid or if outside viewing planes
+    if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
+        return out;
+    }
+    
+    // Swap everything computed so far if computing mirrored half
+    if (mirror) {
+        float4 vTmp = pC; pC = pB; pB = vTmp;
+        vTmp = pD; pD = pA; pA = vTmp;
+        bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
+    }
+    
+    const bool isCap = false;
+    
+    // Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
+    // degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
+    if (aInvalid) { pA = 2.0 * pB - pC; }
+    if (dInvalid) { pD = 2.0 * pC - pB; }
+    bool roundOrCap = isJoinRound || isCap;
+    
+    // Tangent and normal vectors
+    float2 tBC = pC.xy - pB.xy;
+    float lBC = length(tBC);
+    tBC /= lBC;
+    float2 nBC = float2(-tBC.y, tBC.x);
+    
+    float2 tAB = pB.xy - pA.xy;
+    float lAB = length(tAB);
+    if (lAB > 0.0) tAB /= lAB;
+    float2 nAB = float2(-tAB.y, tAB.x);
+    
+    float2 tCD = pD.xy - pC.xy;
+    float lCD = length(tCD);
+    if (lCD > 0.0) tCD /= lCD;
+    float2 nCD = float2(-tCD.y, tCD.x);
+    
+    // Clamp for safety, since we take the arccos
+    float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
+    
+    // This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
+    // of the middle segment. The fix appears straightforward, but this took a few hours to get right.
+    const float tol = 1e-4;
+    float mirrorSign = mirror ? -1.0 : 1.0;
+    float dirB = -dot(tBC, nAB);
+    float dirC = dot(tBC, nCD);
+    bool bCollinear = abs(dirB) < tol;
+    bool cCollinear = abs(dirC) < tol;
+    bool bIsHairpin = bCollinear && cosB < 0.0;
+    // bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
+    dirB = bCollinear ? -mirrorSign : sign(dirB);
+    dirC = cCollinear ? -mirrorSign : sign(dirC);
+    
+    float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
+    
+    // Compute our primary "join index", that is, the index starting at the very first point of the join.
+    // The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
+    float i = mirror ? N - index : index;
+    
+    // Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
+    // that's the only form in which we use this number.
+    float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
+    
+    // Shift the index to send unused vertices to an index below zero, which will then just get clamped to
+    // zero and result in repeated points, i.e. degenerate triangles.
+    i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
+    
+    // Use the direction to offset the index by one. This has the effect of flipping the winding number so
+    // that it's always consistent no matter which direction the join turns.
+    i += (dirB < 0.0 ? -1.0 : 0.0);
+    
+    // Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
+    // in the middle, where the mirrored and unmirrored halves meet.
+    i -= mirror ? 1.0 : 0.0;
+    
+    // Clamp to zero and repeat unused excess vertices.
+    i = max(0.0, i);
+    
+    // Start with a default basis pointing along the segment with normal vector outward
+    float2 xBasis = tBC;
+    float2 yBasis = nBC * dirB;
+    
+    // Default point is 0 along the segment, 1 (width unit) normal to it
+    float2 xy = float2(0);
+    
+    if (i == res + 1.0) {
+        // pick off this one specific index to be the interior miter point
+        // If not div-by-zero, then sinB / (1 + cosB)
+        float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
+        xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
+    } else {
+        // Draw half of a join
+        float m2 = dot(miter, miter);
+        float lm = sqrt(m2);
+        yBasis = miter / lm;
+        xBasis = dirB * float2(yBasis.y, -yBasis.x);
+        bool isBevel = 1.0 > miterLimit * m2;
+        
+        if (((int)i) % 2 == 0) {
+            // Outer joint points
+            if (roundOrCap || i != 0.0) {
+                // Round joins
+                float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
+                xy = float2(cos(theta), sin(theta));
+                
+                if (isCap) {
+                    // A special multiplier factor for turning 3-point rounds into square caps (but leave the
+                    // y == 0.0 point unaffected)
+                    if (xy.y > 0.001) xy *= _capScale;
+                }
+            } else {
+                // Miter joins
+                yBasis = bIsHairpin ? float2(0) : miter;
+                xy.y = isBevel ? 1.0 : 1.0 / m2;
+            }
+        } else {
+            // Offset the center vertex position to get bevel SDF correct
+            if (isBevel && !roundOrCap) {
+                xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
+            }
+        }
+    }
+    
+    // Point offset from main vertex position
+    float2 dP = float2x2(xBasis, yBasis) * xy;
+    
+    // The varying generation code handles clamping, if needed
+    
+    out.position = pB;
+    out.position.xy += width * dP;
+    out.position *= pw;
+    out.position = transform * out.position;
+    
+    return out;
+}
+
+constant static float2 quadVertices[6] = {
+    float2(0.0, 0.0),
+    float2(1.0, 0.0),
+    float2(0.0, 1.0),
+    float2(1.0, 0.0),
+    float2(0.0, 1.0),
+    float2(1.0, 1.0)
+};
+
+struct MetalEngineRectangle {
+    float2 origin;
+    float2 size;
+};
+
+struct MetalEngineQuadVertexOut {
+    float4 position [[position]];
+    float2 uv;
+};
+
+vertex MetalEngineQuadVertexOut blitVertex(
+    const device MetalEngineRectangle &rect [[ buffer(0) ]],
+    unsigned int vid [[ vertex_id ]]
+) {
+    float2 quadVertex = quadVertices[vid];
+    
+    MetalEngineQuadVertexOut out;
+    
+    out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
+    out.position.x = -1.0 + out.position.x * 2.0;
+    out.position.y = -1.0 + out.position.y * 2.0;
+    
+    out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
+    
+    return out;
+}
+
+fragment half4 blitFragment(
+    MetalEngineQuadVertexOut in [[stage_in]],
+    texture2d<half> texture [[ texture(0) ]]
+) {
+    constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
+    half4 color = texture.sample(sampler, in.uv);
+    
+    return half4(color.r, color.g, color.b, color.a);
+}
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift
index bc27655510..f9698f2615 100644
--- a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift
@@ -7,17 +7,566 @@ import AnimatedStickerNode
 import MetalEngine
 import LottieCpp
 import GZip
+import MetalKit
+import HierarchyTrackingLayer
+
+private final class BundleMarker: NSObject {
+}
+
+private var metalLibraryValue: MTLLibrary?
+func metalLibrary(device: MTLDevice) -> MTLLibrary? {
+    if let metalLibraryValue {
+        return metalLibraryValue
+    }
+    
+    let mainBundle = Bundle(for: BundleMarker.self)
+    guard let path = mainBundle.path(forResource: "LottieMetalSourcesBundle", ofType: "bundle") else {
+        return nil
+    }
+    guard let bundle = Bundle(path: path) else {
+        return nil
+    }
+    guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
+        return nil
+    }
+    
+    metalLibraryValue = library
+    return library
+}
+
+private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: Int) -> MTLTexture {
+    let textureDescriptor = MTLTextureDescriptor()
+    textureDescriptor.sampleCount = msaaSampleCount
+    if msaaSampleCount == 1 {
+        textureDescriptor.textureType = .type2D
+    } else {
+        textureDescriptor.textureType = .type2DMultisample
+    }
+    textureDescriptor.width = sideSize
+    textureDescriptor.height = sideSize
+    textureDescriptor.pixelFormat = .bgra8Unorm
+    //textureDescriptor.storageMode = .memoryless
+    textureDescriptor.storageMode = .private
+    textureDescriptor.usage = [.renderTarget, .shaderRead]
+
+    return device.makeTexture(descriptor: textureDescriptor)!
+}
+
+final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject {
+    private var animationContainer: LottieAnimationContainer?
+    var frameIndex: Int = 0
+    
+    var internalData: MetalEngineSubjectInternalData?
+    
+    private var renderBufferHeap: MTLHeap?
+    private var offscreenHeap: MTLHeap?
+    
+    private var multisampleTextureQueue: [MTLTexture] = []
+    
+    private let currentBezierIndicesBuffer = PathRenderBuffer()
+    private let currentBuffer = PathRenderBuffer()
+    
+    final class PrepareState: ComputeState {
+        let pathRenderContext: PathRenderContext
+        
+        init?(device: MTLDevice) {
+            guard let pathRenderContext = PathRenderContext(device: device, msaaSampleCount: 1) else {
+                return nil
+            }
+            self.pathRenderContext = pathRenderContext
+        }
+    }
+    
+    final class RenderState: RenderToLayerState {
+        let pipelineState: MTLRenderPipelineState
+        
+        required init?(device: MTLDevice) {
+            guard let library = metalLibrary(device: device) else {
+                return nil
+            }
+            guard let vertexFunction = library.makeFunction(name: "blitVertex"), let fragmentFunction = library.makeFunction(name: "blitFragment") else {
+                return nil
+            }
+            
+            let pipelineDescriptor = MTLRenderPipelineDescriptor()
+            pipelineDescriptor.vertexFunction = vertexFunction
+            pipelineDescriptor.fragmentFunction = fragmentFunction
+            pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+            guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
+                return nil
+            }
+            self.pipelineState = pipelineState
+        }
+    }
+    
+    init(animationContainer: LottieAnimationContainer) {
+        self.animationContainer = animationContainer
+        
+        #if DEBUG && false
+        let startTime = CFAbsoluteTimeGetCurrent()
+        let buffer = WriteBuffer()
+        for i in 0 ..< animationContainer.animation.frameCount {
+            animationContainer.update(i)
+            serializeNode(buffer: buffer, node: animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)))
+        }
+        buffer.trim()
+        let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
+        let zippedData = TGGZipData(buffer.data, 1.0)
+        print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
+        #endif
+        
+        super.init()
+        
+        self.isOpaque = false
+    }
+    
+    override init(layer: Any) {
+        super.init(layer: layer)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
+        let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
+        
+        path.enumerateItems { pathItem in
+            switch pathItem.pointee.type {
+            case .moveTo:
+                let point = pathItem.pointee.points.0
+                fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
+            case .lineTo:
+                let point = pathItem.pointee.points.0
+                fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
+            case .curveTo:
+                let cp1 = pathItem.pointee.points.0
+                let cp2 = pathItem.pointee.points.1
+                let point = pathItem.pointee.points.2
+                
+                fillState.addCurve(
+                    to: SIMD2<Float>(Float(point.x), Float(point.y)),
+                    cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
+                    cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
+                )
+            case .close:
+                fillState.close()
+            @unknown default:
+                break
+            }
+        }
+        
+        fillState.close()
+        
+        frameState.add(fill: fillState)
+    }
+    
+    private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
+        let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
+        
+        path.enumerateItems { pathItem in
+            switch pathItem.pointee.type {
+            case .moveTo:
+                let point = pathItem.pointee.points.0
+                strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
+            case .lineTo:
+                let point = pathItem.pointee.points.0
+                strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
+            case .curveTo:
+                let cp1 = pathItem.pointee.points.0
+                let cp2 = pathItem.pointee.points.1
+                let point = pathItem.pointee.points.2
+                
+                strokeState.addCurve(
+                    to: SIMD2<Float>(Float(point.x), Float(point.y)),
+                    cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
+                    cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
+                )
+            case .close:
+                strokeState.close()
+            @unknown default:
+                break
+            }
+        }
+        
+        strokeState.complete()
+        
+        frameState.add(stroke: strokeState)
+    }
+    
+    func update(context: MetalEngineSubjectContext) {
+        if self.bounds.isEmpty {
+            return
+        }
+        
+        let size = CGSize(width: 800.0, height: 800.0)
+        let msaaSampleCount = 1
+        
+        let renderSpec = RenderLayerSpec(size: RenderSize(width: Int(size.width), height: Int(size.height)))
+        
+        guard let animationContainer = self.animationContainer else {
+            return
+        }
+        animationContainer.update(self.frameIndex)
+        
+        func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
+            var transform = CATransform3DIdentity
+            transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
+            transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
+            transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
+            transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
+            
+            return transform
+        }
+        
+        let canvasSize = size
+        var transform = defaultTransformForSize(canvasSize)
+        
+        concat(CATransform3DMakeScale(canvasSize.width / animationContainer.animation.size.width, canvasSize.height / animationContainer.animation.size.height, 1.0))
+        
+        var transformStack: [CATransform3D] = []
+        
+        func saveState() {
+            transformStack.append(transform)
+        }
+        
+        func restoreState() {
+            transform = transformStack.removeLast()
+        }
+        
+        func concat(_ other: CATransform3D) {
+            transform = CATransform3DConcat(other, transform)
+        }
+        
+        func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) {
+            if let fill = item.fill {
+                if let solidShading = fill.shading as? LottieRenderContentSolidShading {
+                    self.fillPath(
+                        frameState: frameState,
+                        path: item.path,
+                        shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
+                        rule: fill.fillRule,
+                        transform: transform
+                    )
+                } else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
+                    let gradientType: PathShading.Gradient.GradientType
+                    switch gradientShading.gradientType {
+                    case .linear:
+                        gradientType = .linear
+                    case .radial:
+                        gradientType = .radial
+                    @unknown default:
+                        gradientType = .linear
+                    }
+                    var colorStops: [PathShading.Gradient.ColorStop] = []
+                    for colorStop in gradientShading.colorStops {
+                        colorStops.append(PathShading.Gradient.ColorStop(
+                            color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
+                            location: Float(colorStop.location)
+                        ))
+                    }
+                    let gradientShading = PathShading.Gradient(
+                        gradientType: gradientType,
+                        colorStops: colorStops,
+                        start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
+                        end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
+                    )
+                    self.fillPath(
+                        frameState: frameState,
+                        path: item.path,
+                        shading: .gradient(gradientShading),
+                        rule: fill.fillRule,
+                        transform: transform
+                    )
+                }
+            } else if let stroke = item.stroke {
+                if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
+                    let color = solidShading.color
+                    strokePath(
+                        frameState: frameState,
+                        path: item.path,
+                        width: stroke.lineWidth,
+                        join: stroke.lineJoin,
+                        cap: stroke.lineCap,
+                        miterLimit: stroke.miterLimit,
+                        color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
+                        transform: transform
+                    )
+                }
+            }
+        }
+        
+        func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
+            let normalizedOpacity = node.opacity
+            let layerAlpha = normalizedOpacity * parentAlpha
+            
+            if node.isHidden || normalizedOpacity == 0.0 {
+                return
+            }
+            
+            saveState()
+            
+            var needsTempContext = false
+            if node.mask != nil {
+                needsTempContext = true
+            } else {
+                needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
+            }
+            
+            var maskSurface: PathFrameState.MaskSurface?
+            
+            if needsTempContext {
+                if node.mask != nil || node.masksToBounds {
+                    var maskMode: PathFrameState.MaskSurface.Mode = .regular
+                    
+                    frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
+                    saveState()
+                    
+                    transform = defaultTransformForSize(node.globalRect.size)
+                    concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
+                    concat(node.globalTransform)
+                    
+                    if node.masksToBounds {
+                        let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
+                        
+                        fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
+                        fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
+                        fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
+                        fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
+                        fillState.close()
+                        
+                        frameState.add(fill: fillState)
+                    }
+                    if let maskNode = node.mask {
+                        if maskNode.isInvertedMatte {
+                            maskMode = .inverse
+                        }
+                        renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
+                    }
+                    
+                    restoreState()
+                    
+                    maskSurface = frameState.popOffscreenMask(mode: maskMode)
+                }
+                
+                frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
+                saveState()
+                
+                transform = defaultTransformForSize(node.globalRect.size)
+                concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
+                concat(node.globalTransform)
+            } else {
+                concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
+                concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
+                concat(node.transform)
+            }
+            
+            var renderAlpha: CGFloat = 1.0
+            if needsTempContext {
+                renderAlpha = 1.0
+            } else {
+                renderAlpha = layerAlpha
+            }
+            
+            if let renderContent = node.renderContent {
+                renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha)
+            }
+            
+            for subnode in node.subnodes {
+                renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
+            }
+            
+            if needsTempContext {
+                restoreState()
+                
+                concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
+                concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
+                concat(node.transform)
+                concat(CATransform3DInvert(node.globalTransform))
+                
+                frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
+            }
+            
+            restoreState()
+        }
+        
+        self.currentBuffer.reset()
+        self.currentBezierIndicesBuffer.reset()
+        let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: 1, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer)
+        
+        let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0))
+        renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0)
+        
+        final class ComputeOutput {
+            let pathRenderContext: PathRenderContext
+            let renderBufferHeap: MTLHeap
+            let multisampleTexture: MTLTexture
+            let takenMultisampleTextures: [MTLTexture]
+            
+            init(pathRenderContext: PathRenderContext, renderBufferHeap: MTLHeap, multisampleTexture: MTLTexture, takenMultisampleTextures: [MTLTexture]) {
+                self.pathRenderContext = pathRenderContext
+                self.renderBufferHeap = renderBufferHeap
+                self.multisampleTexture = multisampleTexture
+                self.takenMultisampleTextures = takenMultisampleTextures
+            }
+        }
+        
+        var customCompletion: (() -> Void)?
+        
+        let computeOutput = context.compute(state: PrepareState.self, commands: { commandBuffer, state -> ComputeOutput? in
+            let renderBufferHeap: MTLHeap
+            if let current = self.renderBufferHeap {
+                renderBufferHeap = current
+            } else {
+                let heapDescriptor = MTLHeapDescriptor()
+                heapDescriptor.size = 32 * 1024 * 1024
+                heapDescriptor.storageMode = .shared
+                heapDescriptor.cpuCacheMode = .writeCombined
+                if #available(iOS 13.0, *) {
+                    heapDescriptor.hazardTrackingMode = .tracked
+                }
+                guard let value = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor) else {
+                    print()
+                    return nil
+                }
+                self.renderBufferHeap = value
+                renderBufferHeap = value
+            }
+            
+            guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
+                return nil
+            }
+            
+            frameState.prepare(heap: renderBufferHeap)
+            frameState.encodeCompute(context: state.pathRenderContext, computeEncoder: computeEncoder)
+            
+            computeEncoder.endEncoding()
+            
+            let multisampleTexture: MTLTexture
+            if !self.multisampleTextureQueue.isEmpty {
+                multisampleTexture = self.multisampleTextureQueue.removeFirst()
+            } else {
+                multisampleTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1)
+            }
+            
+            let tempTexture: MTLTexture
+            if !self.multisampleTextureQueue.isEmpty {
+                tempTexture = self.multisampleTextureQueue.removeFirst()
+            } else {
+                tempTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1)
+            }
+            
+            let renderPassDescriptor = MTLRenderPassDescriptor()
+            renderPassDescriptor.colorAttachments[0].texture = multisampleTexture
+            if msaaSampleCount == 1 {
+                renderPassDescriptor.colorAttachments[0].storeAction = .store
+            } else {
+                //renderPassDescriptor.colorAttachments[0].resolveTexture = self.currentDrawable?.texture
+                renderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve
+                preconditionFailure()
+            }
+            renderPassDescriptor.colorAttachments[0].loadAction = .clear
+            renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
+            
+            renderPassDescriptor.colorAttachments[1].texture = tempTexture
+            renderPassDescriptor.colorAttachments[1].loadAction = .clear
+            renderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
+            renderPassDescriptor.colorAttachments[1].storeAction = .dontCare
+            
+            if msaaSampleCount == 4 {
+                renderPassDescriptor.setSamplePositions([
+                    MTLSamplePosition(x: 0.25, y: 0.25),
+                    MTLSamplePosition(x: 0.75, y: 0.25),
+                    MTLSamplePosition(x: 0.75, y: 0.75),
+                    MTLSamplePosition(x: 0.25, y: 0.75)
+                ])
+            }
+            
+            var offscreenHeapMemorySize = frameState.calculateOffscreenHeapMemorySize(device: MetalEngine.shared.device)
+            offscreenHeapMemorySize = max(offscreenHeapMemorySize, 1 * 1024 * 1024)
+            
+            let offscreenHeap: MTLHeap
+            if let current = self.offscreenHeap, current.size >= offscreenHeapMemorySize * 3 {
+                offscreenHeap = current
+            } else {
+                print("Creating offscreen heap \(offscreenHeapMemorySize * 3 / (1024 * 1024)) MB (3 * \(offscreenHeapMemorySize / (1024 * 1024)) MB)")
+                let heapDescriptor = MTLHeapDescriptor()
+                heapDescriptor.size = offscreenHeapMemorySize * 3
+                heapDescriptor.storageMode = .private
+                heapDescriptor.cpuCacheMode = .defaultCache
+                if #available(iOS 13.0, *) {
+                    heapDescriptor.hazardTrackingMode = .tracked
+                }
+                offscreenHeap = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor)!
+                self.offscreenHeap = offscreenHeap
+            }
+            
+            frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize)
+            
+            guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
+                self.multisampleTextureQueue.append(multisampleTexture)
+                self.multisampleTextureQueue.append(tempTexture)
+                return nil
+            }
+            
+            frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize)
+            
+            renderEncoder.endEncoding()
+            
+            return ComputeOutput(
+                pathRenderContext: state.pathRenderContext,
+                renderBufferHeap: renderBufferHeap,
+                multisampleTexture: multisampleTexture,
+                takenMultisampleTextures: [multisampleTexture, tempTexture]
+            )
+        })
+        
+        context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: computeOutput, commands: { [weak self] encoder, placement, computeOutput in
+            guard let computeOutput else {
+                return
+            }
+            
+            let effectiveRect = placement.effectiveRect
+            
+            var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
+            encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
+            
+            encoder.setFragmentTexture(computeOutput.multisampleTexture, index: 0)
+            encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
+            
+            let takenMultisampleTextures = computeOutput.takenMultisampleTextures
+            customCompletion = {
+                guard let self else {
+                    return
+                }
+                for texture in takenMultisampleTextures {
+                    self.multisampleTextureQueue.append(texture)
+                }
+            }
+        })
+        
+        context.addCustomCompletion({
+            customCompletion?()
+        })
+    }
+}
 
 public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
     private final class LoadFrameTask {
         var isCancelled: Bool = false
     }
     
+    private let hierarchyTrackingLayer: HierarchyTrackingLayer
+    
     public var automaticallyLoadFirstFrame: Bool = false
     public var automaticallyLoadLastFrame: Bool = false
     public var playToCompletionOnStop: Bool = false
     
+    private var layoutSize: CGSize?
     private var lottieInstance: LottieAnimationContainer?
+    private var renderLayer: LottieContentLayer?
+    
+    private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
     
     private var didStart: Bool = false
     public var started: () -> Void = {}
@@ -73,12 +622,26 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
     private var playbackMode: AnimatedStickerPlaybackMode = .loop
     
     override public init() {
+        self.hierarchyTrackingLayer = HierarchyTrackingLayer()
+        
         super.init()
         
-        self.backgroundColor = .blue
+        self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
+            guard let self else {
+                return
+            }
+            self.updatePlayback()
+        }
+        self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
+            guard let self else {
+                return
+            }
+            self.updatePlayback()
+        }
     }
     
     deinit {
+        self.sourceDisposable?.dispose()
     }
     
     public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
@@ -124,6 +687,55 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
             self.isPlaying = isPlaying
             self.isPlayingChanged(self.isPlaying)
         }
+        
+        if isPlaying, let lottieInstance = self.lottieInstance {
+            if self.displayLinkSubscription == nil {
+                let fps: Int
+                if lottieInstance.animation.framesPerSecond == 30 {
+                    fps = 30
+                } else {
+                    fps = 60
+                }
+                self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(fps), { [weak self] deltaTime in
+                    guard let self, let lottieInstance = self.lottieInstance, let renderLayer = self.renderLayer else {
+                        return
+                    }
+                    if renderLayer.frameIndex == lottieInstance.animation.frameCount - 1 {
+                        switch self.playbackMode {
+                        case .loop:
+                            self.completed(false)
+                        case let .count(count):
+                            if count <= 1 {
+                                if !self.didComplete {
+                                    self.didComplete = true
+                                    self.completed(true)
+                                }
+                                return
+                            } else {
+                                self.playbackMode = .count(count - 1)
+                                self.completed(false)
+                            }
+                        case .once:
+                            if !self.didComplete {
+                                self.didComplete = true
+                                self.completed(true)
+                            }
+                            return
+                        case .still:
+                            break
+                        }
+                    }
+                    
+                    self.frameIndex = (self.frameIndex + 1) % lottieInstance.animation.frameCount
+                    renderLayer.frameIndex = self.frameIndex
+                    renderLayer.setNeedsUpdate()
+                })
+                
+                self.renderLayer?.setNeedsUpdate()
+            }
+        } else {
+            self.displayLinkSubscription = nil
+        }
     }
     
     private func advanceFrameIfPossible() {
@@ -173,6 +785,13 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
     private func setupPlayback(lottieInstance: LottieAnimationContainer) {
         self.lottieInstance = lottieInstance
         
+        let renderLayer = LottieContentLayer(animationContainer: lottieInstance)
+        self.renderLayer = renderLayer
+        if let layoutSize = self.layoutSize {
+            renderLayer.frame = CGRect(origin: CGPoint(), size: layoutSize)
+        }
+        self.layer.addSublayer(renderLayer)
+        
         self.updatePlayback()
     }
     
@@ -205,6 +824,10 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
     }
     
     public func updateLayout(size: CGSize) {
+        self.layoutSize = size
+        if let renderLayer = self.renderLayer {
+            renderLayer.frame = CGRect(origin: CGPoint(), size: size)
+        }
     }
     
     public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift
new file mode 100644
index 0000000000..9013fff1da
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift
@@ -0,0 +1,374 @@
+import Foundation
+import MetalKit
+import LottieCpp
+
+private func alignUp(size: Int, align: Int) -> Int {
+    precondition(((align - 1) & align) == 0, "Align must be a power of two")
+
+    let alignmentMask = align - 1
+    return (size + alignmentMask) & ~alignmentMask
+}
+
+final class PathFrameState {
+    struct RenderItem {
+        enum Content {
+            case fill(PathRenderFillState)
+            case stroke(PathRenderStrokeState)
+            case offscreen(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?)
+            
+            func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer, canvasSize: CGSize) {
+                switch self {
+                case let .fill(fill):
+                    fill.encode(context: context, encoder: encoder, buffer: buffer)
+                case let .stroke(stroke):
+                    stroke.encode(context: context, encoder: encoder, buffer: buffer)
+                case let .offscreen(surface, rect, transform, opacity, mask):
+                    surface.encode(context: context, encoder: encoder, canvasSize: canvasSize, rect: rect, transform: transform, opacity: opacity, mask: mask)
+                }
+            }
+        }
+        
+        let content: Content
+        
+        init(content: Content) {
+            self.content = content
+        }
+    }
+    
+    final class MaskSurface {
+        enum Mode {
+            case regular
+            case inverse
+        }
+        
+        let surface: Surface
+        let mode: Mode
+        
+        init(surface: Surface, mode: Mode) {
+            self.surface = surface
+            self.mode = mode
+        }
+    }
+    
+    final class Surface {
+        let width: Int
+        let height: Int
+        private let msaaSampleCount: Int
+        
+        private var texture: MTLTexture?
+        
+        private(set) var items: [RenderItem] = []
+        
+        init(width: Int, height: Int, msaaSampleCount: Int) {
+            self.width = width
+            self.height = height
+            self.msaaSampleCount = msaaSampleCount
+        }
+        
+        func add(fill: PathRenderFillState) {
+            self.items.append(RenderItem(content: .fill(fill)))
+        }
+        
+        func add(stroke: PathRenderStrokeState) {
+            self.items.append(RenderItem(content: .stroke(stroke)))
+        }
+        
+        func add(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
+            self.items.append(RenderItem(content: .offscreen(surface: surface, rect: rect, transform: transform, opacity: opacity, mask: mask)))
+        }
+        
+        func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
+            guard let texture = self.texture else {
+                print("Trying to encode offscreen blit pass, but no texture is present")
+                return
+            }
+            if mask != nil {
+                encoder.setRenderPipelineState(context.drawOffscreenWithMaskPipelineState)
+            } else {
+                encoder.setRenderPipelineState(context.drawOffscreenPipelineState)
+            }
+            
+            let identityTransform = CATransform3DIdentity
+            var identityTransformMatrix = SIMD16<Float>(
+                Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
+                Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
+                Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
+                Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
+            )
+            
+            let boundingBox = rect.applying(CATransform3DGetAffineTransform(transform))
+            
+            var quadVertices: [SIMD4<Float>] = [
+                SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
+                SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
+                SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
+                
+                SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
+                SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
+                SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
+            ]
+            
+            encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
+            encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
+            encoder.setFragmentTexture(texture, index: 0)
+            if let mask {
+                guard let maskTexture = mask.surface.texture else {
+                    print("Trying to encode offscreen blit pass, but no mask texture is present")
+                    return
+                }
+                encoder.setFragmentTexture(maskTexture, index: 1)
+            }
+            var opacity = opacity
+            encoder.setFragmentBytes(&opacity, length: 4, index: 1)
+            
+            if let mask {
+                var maskMode: UInt32 = mask.mode == .regular ? 0 : 1;
+                encoder.setFragmentBytes(&maskMode, length: 4, index: 2)
+            }
+            encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
+        }
+        
+        func offscreenTextureDescriptor() -> MTLTextureDescriptor {
+            let textureDescriptor = MTLTextureDescriptor()
+            textureDescriptor.textureType = .type2D
+            textureDescriptor.width = self.width
+            textureDescriptor.height = self.height
+            textureDescriptor.pixelFormat = .bgra8Unorm
+            textureDescriptor.storageMode = .private
+            textureDescriptor.usage = [.renderTarget, .shaderRead]
+            return textureDescriptor
+        }
+        
+        func offscreenTempTextureDescriptor() -> MTLTextureDescriptor {
+            let tempTextureDescriptor = MTLTextureDescriptor()
+            tempTextureDescriptor.sampleCount = self.msaaSampleCount
+            if self.msaaSampleCount == 1 {
+                tempTextureDescriptor.textureType = .type2D
+            } else {
+                tempTextureDescriptor.textureType = .type2DMultisample
+            }
+            tempTextureDescriptor.width = self.width
+            tempTextureDescriptor.height = self.height
+            tempTextureDescriptor.pixelFormat = .bgra8Unorm
+            tempTextureDescriptor.storageMode = .private
+            tempTextureDescriptor.usage = [.renderTarget, .shaderRead]
+            return tempTextureDescriptor
+        }
+        
+        func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
+            var result = 0
+            
+            var sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTextureDescriptor())
+            result += sizeAndAlign.size
+            
+            sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTempTextureDescriptor())
+            result += sizeAndAlign.size * 2
+            
+            for item in self.items {
+                if case let .offscreen(surface, _, _, _, mask) = item.content {
+                    result += surface.calculateOffscreenHeapMemorySize(device: device)
+                    if let mask {
+                        result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
+                    }
+                }
+            }
+            return result
+        }
+        
+        func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, materializedBuffer: MTLBuffer, canvasSize: CGSize) {
+            guard let resultTexture = heap.makeTexture(descriptor: self.offscreenTextureDescriptor()) else {
+                return
+            }
+            
+            for item in self.items {
+                if case let .offscreen(surface, _, _, _, mask) = item.content {
+                    if let mask {
+                        mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
+                    }
+                    surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
+                }
+            }
+            
+            self.texture = resultTexture
+            
+            guard let offscreenTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
+                return
+            }
+            guard let tempTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
+                return
+            }
+            
+            let offscreenRenderPassDescriptor = MTLRenderPassDescriptor()
+            if msaaSampleCount == 1 {
+                offscreenRenderPassDescriptor.colorAttachments[0].texture = resultTexture
+                offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .store
+            } else {
+                offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture
+                offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve
+                offscreenRenderPassDescriptor.colorAttachments[0].resolveTexture = resultTexture
+            }
+            offscreenRenderPassDescriptor.colorAttachments[0].loadAction = .clear
+            offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
+            
+            offscreenRenderPassDescriptor.colorAttachments[1].texture = tempTexture
+            offscreenRenderPassDescriptor.colorAttachments[1].loadAction = .clear
+            offscreenRenderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
+            offscreenRenderPassDescriptor.colorAttachments[1].storeAction = .dontCare
+            
+            if self.msaaSampleCount == 4 {
+                offscreenRenderPassDescriptor.setSamplePositions([
+                    MTLSamplePosition(x: 0.25, y: 0.25),
+                    MTLSamplePosition(x: 0.75, y: 0.25),
+                    MTLSamplePosition(x: 0.75, y: 0.75),
+                    MTLSamplePosition(x: 0.25, y: 0.75)
+                ])
+            }
+            
+            guard let offscreenRenderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: offscreenRenderPassDescriptor) else {
+                return
+            }
+            
+            for item in self.items {
+                item.content.encode(context: context, encoder: offscreenRenderEncoder, buffer: materializedBuffer, canvasSize: canvasSize)
+            }
+            
+            offscreenRenderEncoder.endEncoding()
+        }
+    }
+    
+    let msaaSampleCount: Int
+    let buffer: PathRenderBuffer
+    let bezierDataBuffer: PathRenderBuffer
+    
+    private var surfaceStack: [Surface] = []
+    
+    private var materializedBuffer: MTLBuffer?
+    private var materializedBezierIndexBuffer: MTLBuffer?
+    
+    init(width: Int, height: Int, msaaSampleCount: Int, buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer) {
+        self.msaaSampleCount = msaaSampleCount
+        self.buffer = buffer
+        self.bezierDataBuffer = bezierDataBuffer
+        self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: msaaSampleCount))
+    }
+    
+    func pushOffscreen(width: Int, height: Int) {
+        self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: self.msaaSampleCount))
+    }
+    
+    func popOffscreen(rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface? = nil) {
+        self.surfaceStack[self.surfaceStack.count - 2].add(surface: self.surfaceStack[self.surfaceStack.count - 1], rect: rect, transform: transform, opacity: opacity, mask: mask)
+        self.surfaceStack.removeLast()
+    }
+    
+    func popOffscreenMask(mode: MaskSurface.Mode) -> MaskSurface {
+        return MaskSurface(
+            surface: self.surfaceStack.removeLast(),
+            mode: mode
+        )
+    }
+    
+    func add(fill: PathRenderFillState) {
+        self.surfaceStack.last!.add(fill: fill)
+    }
+    
+    func add(stroke: PathRenderStrokeState) {
+        self.surfaceStack.last!.add(stroke: stroke)
+    }
+    
+    func prepare(heap: MTLHeap) {
+        if self.buffer.length == 0 {
+            return
+        }
+        
+        var bufferOptions: MTLResourceOptions = [.storageModeShared, .cpuCacheModeWriteCombined]
+        if #available(iOS 13.0, *) {
+            bufferOptions.insert(.hazardTrackingModeTracked)
+        }
+        
+        guard let materializedBuffer = heap.makeBuffer(length: self.buffer.length, options: bufferOptions) else {
+            print("Could not create materialized buffer")
+            return
+        }
+        materializedBuffer.label = "materializedBuffer"
+        self.materializedBuffer = materializedBuffer
+        
+        memcpy(materializedBuffer.contents(), self.buffer.memory, self.buffer.length)
+        
+        if self.bezierDataBuffer.length != 0 {
+            guard let materializedBezierIndexBuffer = heap.makeBuffer(length: self.bezierDataBuffer.length, options: bufferOptions) else {
+                print("Could not create materialized bezier index buffer")
+                return
+            }
+            self.materializedBezierIndexBuffer = materializedBezierIndexBuffer
+            materializedBezierIndexBuffer.label = "materializedBezierIndexBuffer"
+            
+            memcpy(materializedBezierIndexBuffer.contents(), self.bezierDataBuffer.memory, self.bezierDataBuffer.length)
+        }
+    }
+    
+    func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
+        var result = 0
+        for item in self.surfaceStack[0].items {
+            if case let .offscreen(surface, _, _, _, mask) = item.content {
+                result += surface.calculateOffscreenHeapMemorySize(device: device)
+                if let mask {
+                    result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
+                }
+            }
+        }
+        return result
+    }
+    
+    func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, canvasSize: CGSize) {
+        guard let materializedBuffer = self.materializedBuffer else {
+            return
+        }
+        
+        assert(self.surfaceStack.count == 1)
+        
+        for item in self.surfaceStack[0].items {
+            if case let .offscreen(surface, _, _, _, mask) = item.content {
+                if let mask {
+                    mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
+                }
+                surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
+            }
+        }
+    }
+    
+    func encodeRender(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize) {
+        guard let materializedBuffer = self.materializedBuffer else {
+            return
+        }
+        
+        assert(self.surfaceStack.count == 1)
+        
+        for item in self.surfaceStack[0].items {
+            item.content.encode(context: context, encoder: encoder, buffer: materializedBuffer, canvasSize: canvasSize)
+        }
+    }
+    
+    func encodeCompute(context: PathRenderContext, computeEncoder: MTLComputeCommandEncoder) {
+        guard let materializedBuffer = self.materializedBuffer, let materializedBezierIndexBuffer = self.materializedBezierIndexBuffer else {
+            return
+        }
+        
+        let itemSize = 4 + 4 * 4 * 2 + 4
+        let itemCount = self.bezierDataBuffer.length / itemSize
+        
+        computeEncoder.setComputePipelineState(context.prepareBezierPipelineState)
+        
+        let threadGroupWidth = 16
+        let threadGroupHeight = 8
+        
+        computeEncoder.useResource(materializedBuffer, usage: .write)
+        
+        computeEncoder.setBuffer(materializedBezierIndexBuffer, offset: 0, index: 0)
+        computeEncoder.setBuffer(materializedBuffer, offset: 0, index: 1)
+        var itemCountSize: UInt32 = UInt32(itemCount)
+        computeEncoder.setBytes(&itemCountSize, length: 4, index: 2)
+        let dispatchSize = alignUp(size: itemCount, align: threadGroupWidth)
+        computeEncoder.dispatchThreadgroups(MTLSize(width: dispatchSize, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: threadGroupHeight, depth: 1))
+    }
+}
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderBuffer.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderBuffer.swift
new file mode 100644
index 0000000000..3190d0aa62
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderBuffer.swift
@@ -0,0 +1,77 @@
+import Foundation
+import MetalKit
+import LottieCpp
+
+final class PathRenderBuffer {
+    private(set) var memory: UnsafeMutableRawPointer
+    private(set) var capacity: Int = 8 * 1024 * 1024
+    private(set) var length: Int = 0
+    
+    init() {
+        self.memory = malloc(self.capacity)!
+    }
+    
+    func reset() {
+        self.length = 0
+    }
+    
+    func append(bytes: UnsafeRawPointer, length: Int) {
+        assert(length % 4 == 0)
+        
+        if self.length + length > self.capacity {
+            self.capacity = self.capacity * 2
+            preconditionFailure()
+        }
+        memcpy(self.memory.advanced(by: self.length), bytes, length)
+        self.length += length
+    }
+    
+    func appendZero(count: Int) {
+        if self.length + length > self.capacity {
+            self.capacity = self.capacity * 2
+            preconditionFailure()
+        }
+        self.length += count
+    }
+    
+    func append(float: Float) {
+        var value: Float = float
+        self.append(bytes: &value, length: 4)
+    }
+    
+    func append(float2: SIMD2<Float>) {
+        var value: SIMD2<Float> = float2
+        self.append(bytes: &value, length: 4 * 2)
+    }
+    
+    func append(float3: SIMD3<Float>) {
+        var value = float3.x
+        self.append(bytes: &value, length: 4)
+        value = float3.y
+        self.append(bytes: &value, length: 4)
+        value = float3.z
+        self.append(bytes: &value, length: 4)
+    }
+    
+    func append(int: Int32) {
+        var value = int
+        self.append(bytes: &value, length: 4)
+    }
+    
+    func appendBezierData(
+        bufferOffset: Int,
+        start: SIMD2<Float>,
+        end: SIMD2<Float>,
+        cp1: SIMD2<Float>,
+        cp2: SIMD2<Float>,
+        offset: Float
+    ) {
+        self.append(int: Int32(bufferOffset))
+        self.append(float2: start)
+        self.append(float2: end)
+        self.append(float2: cp1)
+        self.append(float2: cp2)
+        self.append(float: offset)
+    }
+}
+
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderContext.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderContext.swift
new file mode 100644
index 0000000000..524dda3006
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderContext.swift
@@ -0,0 +1,208 @@
+import Foundation
+import MetalKit
+import LottieCpp
+
+final class PathRenderContext {
+    let device: MTLDevice
+    let msaaSampleCount: Int
+    
+    let prepareBezierPipelineState: MTLComputePipelineState
+    let shapePipelineState: MTLRenderPipelineState
+    let clearPipelineState: MTLRenderPipelineState
+    let mergeColorFillPipelineState: MTLRenderPipelineState
+    let mergeLinearGradientFillPipelineState: MTLRenderPipelineState
+    let mergeRadialGradientFillPipelineState: MTLRenderPipelineState
+    let strokeTerminalPipelineState: MTLRenderPipelineState
+    let strokeInnerPipelineState: MTLRenderPipelineState
+    let drawOffscreenPipelineState: MTLRenderPipelineState
+    let drawOffscreenWithMaskPipelineState: MTLRenderPipelineState
+    
+    let maximumThreadGroupWidth: Int
+    
+    init?(device: MTLDevice, msaaSampleCount: Int) {
+        self.device = device
+        self.msaaSampleCount = msaaSampleCount
+        
+        self.maximumThreadGroupWidth = device.maxThreadsPerThreadgroup.width
+        
+        guard let library = metalLibrary(device: device) else {
+            return nil
+        }
+        
+        guard let quadVertexFunction = library.makeFunction(name: "quad_vertex_shader") else {
+            print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let shapeVertexFunction = library.makeFunction(name: "fill_vertex_shader") else {
+            print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let shapeFragmentFunction = library.makeFunction(name: "fragment_shader") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let clearFragmentFunction = library.makeFunction(name: "clear_mask_fragment") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let mergeColorFillFragmentFunction = library.makeFunction(name: "merge_color_fill_fragment_shader") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let mergeLinearGradientFillFragmentFunction = library.makeFunction(name: "merge_linear_gradient_fill_fragment_shader") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let mergeRadialGradientFillFragmentFunction = library.makeFunction(name: "merge_radial_gradient_fill_fragment_shader") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let strokeFragmentFunction = library.makeFunction(name: "stroke_fragment_shader") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let strokeTerminalVertexFunction = library.makeFunction(name: "strokeTerminalVertex") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let strokeInnerVertexFunction = library.makeFunction(name: "strokeInnerVertex") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let prepareBezierPipelineFunction = library.makeFunction(name: "evaluateBezier") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let quadOffscreenFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        guard let quadOffscreenWithMaskFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment_with_mask") else {
+            print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
+            return nil
+        }
+        
+        self.prepareBezierPipelineState = try! device.makeComputePipelineState(function: prepareBezierPipelineFunction)
+        
+        let shapePipelineDescriptor = MTLRenderPipelineDescriptor()
+        shapePipelineDescriptor.vertexFunction = shapeVertexFunction
+        shapePipelineDescriptor.fragmentFunction = shapeFragmentFunction
+        
+        shapePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+        shapePipelineDescriptor.colorAttachments[0].writeMask = []
+        shapePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
+        shapePipelineDescriptor.colorAttachments[1].writeMask = [.all]
+        shapePipelineDescriptor.rasterSampleCount = msaaSampleCount
+
+        guard let shapePipelineState = try? device.makeRenderPipelineState(descriptor: shapePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.shapePipelineState = shapePipelineState
+        
+        let clearPipelineDescriptor = MTLRenderPipelineDescriptor()
+        clearPipelineDescriptor.vertexFunction = quadVertexFunction
+        clearPipelineDescriptor.fragmentFunction = clearFragmentFunction
+        
+        clearPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+        clearPipelineDescriptor.colorAttachments[0].writeMask = []
+        clearPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
+        clearPipelineDescriptor.colorAttachments[1].writeMask = .all
+        clearPipelineDescriptor.rasterSampleCount = msaaSampleCount
+
+        guard let clearPipelineState = try? device.makeRenderPipelineState(descriptor: clearPipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.clearPipelineState = clearPipelineState
+        
+        let mergePipelineDescriptor = MTLRenderPipelineDescriptor()
+        mergePipelineDescriptor.vertexFunction = quadVertexFunction
+        mergePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+        mergePipelineDescriptor.colorAttachments[0].writeMask = [.all]
+        mergePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
+        mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
+        mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
+        mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
+        mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
+        mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
+        mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
+        mergePipelineDescriptor.rasterSampleCount = msaaSampleCount
+        
+        mergePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
+        mergePipelineDescriptor.colorAttachments[1].writeMask = []
+        
+        mergePipelineDescriptor.fragmentFunction = mergeColorFillFragmentFunction
+        guard let mergeColorFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.mergeColorFillPipelineState = mergeColorFillPipelineState
+        
+        mergePipelineDescriptor.fragmentFunction = mergeLinearGradientFillFragmentFunction
+        guard let mergeLinearGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.mergeLinearGradientFillPipelineState = mergeLinearGradientFillPipelineState
+        
+        mergePipelineDescriptor.fragmentFunction = mergeRadialGradientFillFragmentFunction
+        guard let mergeRadialGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.mergeRadialGradientFillPipelineState = mergeRadialGradientFillPipelineState
+        
+        let strokePipelineDescriptor = MTLRenderPipelineDescriptor()
+        strokePipelineDescriptor.fragmentFunction = strokeFragmentFunction
+        strokePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+        strokePipelineDescriptor.colorAttachments[0].writeMask = [.all]
+        strokePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
+        strokePipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
+        strokePipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
+        strokePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
+        strokePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
+        strokePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
+        strokePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
+        strokePipelineDescriptor.rasterSampleCount = msaaSampleCount
+        
+        strokePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
+        strokePipelineDescriptor.colorAttachments[1].writeMask = []
+        strokePipelineDescriptor.vertexFunction = strokeTerminalVertexFunction
+        guard let strokeTerminalPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.strokeTerminalPipelineState = strokeTerminalPipelineState
+        
+        strokePipelineDescriptor.vertexFunction = strokeInnerVertexFunction
+        guard let strokeInnerPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.strokeInnerPipelineState = strokeInnerPipelineState
+        
+        let drawOffscreenPipelineDescriptor = MTLRenderPipelineDescriptor()
+        drawOffscreenPipelineDescriptor.vertexFunction = quadVertexFunction
+        
+        drawOffscreenPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+        drawOffscreenPipelineDescriptor.colorAttachments[0].writeMask = [.all]
+        drawOffscreenPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
+        drawOffscreenPipelineDescriptor.colorAttachments[1].writeMask = []
+        drawOffscreenPipelineDescriptor.rasterSampleCount = msaaSampleCount
+        
+        drawOffscreenPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
+        drawOffscreenPipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
+        drawOffscreenPipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
+        drawOffscreenPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
+        drawOffscreenPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
+        drawOffscreenPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
+        drawOffscreenPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
+        
+        drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenFragmentFunction
+        guard let drawOffscreenPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.drawOffscreenPipelineState = drawOffscreenPipelineState
+        
+        drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenWithMaskFragmentFunction
+        guard let drawOffscreenWithMaskPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
+            preconditionFailure()
+        }
+        self.drawOffscreenWithMaskPipelineState = drawOffscreenWithMaskPipelineState
+    }
+}
+
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift
new file mode 100644
index 0000000000..f7f22cdd56
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift
@@ -0,0 +1,277 @@
+import Foundation
+import MetalKit
+import simd
+import LottieCpp
+    
+enum PathShading {
+    final class Gradient {
+        enum GradientType {
+            case linear
+            case radial
+        }
+        
+        struct ColorStop {
+            var color: LottieColor
+            var location: Float
+            
+            init(color: LottieColor, location: Float) {
+                self.color = color
+                self.location = location
+            }
+        }
+        
+        let gradientType: GradientType
+        let colorStops: [ColorStop]
+        let start: SIMD2<Float>
+        let end: SIMD2<Float>
+        
+        init(gradientType: GradientType, colorStops: [ColorStop], start: SIMD2<Float>, end: SIMD2<Float>) {
+            self.gradientType = gradientType
+            self.colorStops = colorStops
+            self.start = start
+            self.end = end
+        }
+    }
+    
+    case color(LottieColor)
+    case gradient(Gradient)
+}
+
+final class PathRenderSubpathFillState {
+    private let buffer: PathRenderBuffer
+    private let bezierDataBuffer: PathRenderBuffer
+    let bufferOffset: Int
+    private(set) var vertexCount: Int = 0
+    
+    private var firstPosition: SIMD2<Float>
+    private var lastPosition: SIMD2<Float>
+    
+    private(set) var minPosition: SIMD2<Float>
+    private(set) var maxPosition: SIMD2<Float>
+    
+    private var isClosed: Bool = false
+    
+    init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
+        self.buffer = buffer
+        self.bezierDataBuffer = bezierDataBuffer
+        self.bufferOffset = buffer.length
+        
+        self.firstPosition = point
+        self.lastPosition = point
+        self.minPosition = point
+        self.maxPosition = point
+        
+        self.add(point: point)
+    }
+    
+    func add(point: SIMD2<Float>) {
+        self.buffer.append(float2: point)
+        
+        self.minPosition.x = min(self.minPosition.x, point.x)
+        self.minPosition.y = min(self.minPosition.y, point.y)
+        self.maxPosition.x = max(self.maxPosition.x, point.x)
+        self.maxPosition.y = max(self.maxPosition.y, point.y)
+        
+        self.lastPosition = point
+        
+        self.vertexCount += 1
+    }
+    
+    func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
+        let stepCount = 8
+        self.bezierDataBuffer.appendBezierData(
+            bufferOffset: self.buffer.length / 4,
+            start: self.lastPosition,
+            end: point,
+            cp1: cp1,
+            cp2: cp2,
+            offset: 0.0
+        )
+        self.buffer.appendZero(count: 4 * 2 * stepCount)
+        self.vertexCount += stepCount
+        
+        let (curveMin, curveMax) = bezierBounds(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point)
+        
+        self.minPosition.x = min(self.minPosition.x, curveMin.x)
+        self.minPosition.y = min(self.minPosition.y, curveMin.y)
+        self.maxPosition.x = max(self.maxPosition.x, curveMax.x)
+        self.maxPosition.y = max(self.maxPosition.y, curveMax.y)
+        
+        self.lastPosition = point
+    }
+    
+    func close() {
+        if self.isClosed {
+            assert(false)
+        } else {
+            self.isClosed = true
+            
+            if self.lastPosition != self.firstPosition {
+                self.add(point: self.firstPosition)
+            }
+        }
+    }
+}
+
+final class PathRenderFillState {
+    private let buffer: PathRenderBuffer
+    private let bezierDataBuffer: PathRenderBuffer
+    private let fillRule: LottieFillRule
+    private let shading: PathShading
+    private let transform: CATransform3D
+    
+    private var currentSubpath: PathRenderSubpathFillState?
+    private(set) var subpaths: [PathRenderSubpathFillState] = []
+    
+    init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, fillRule: LottieFillRule, shading: PathShading, transform: CATransform3D) {
+        self.buffer = buffer
+        self.bezierDataBuffer = bezierDataBuffer
+        self.fillRule = fillRule
+        self.shading = shading
+        self.transform = transform
+    }
+    
+    func begin(point: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.close()
+            self.subpaths.append(currentSubpath)
+            self.currentSubpath = nil
+        }
+        
+        self.currentSubpath = PathRenderSubpathFillState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
+    }
+    
+    func addLine(to point: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.add(point: point)
+        }
+    }
+    
+    func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
+        }
+    }
+    
+    func close() {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.close()
+            self.subpaths.append(currentSubpath)
+            self.currentSubpath = nil
+        }
+    }
+    
+    func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
+        if self.subpaths.isEmpty {
+            return
+        }
+        var minPosition: SIMD2<Float> = self.subpaths[0].minPosition
+        var maxPosition: SIMD2<Float> = self.subpaths[0].maxPosition
+        for subpath in self.subpaths {
+            minPosition.x = min(minPosition.x, subpath.minPosition.x)
+            minPosition.y = min(minPosition.y, subpath.minPosition.y)
+            maxPosition.x = max(maxPosition.x, subpath.maxPosition.x)
+            maxPosition.y = max(maxPosition.y, subpath.maxPosition.y)
+        }
+        
+        let localBoundingBox = CGRect(x: CGFloat(minPosition.x), y: CGFloat(minPosition.y), width: CGFloat(maxPosition.x - minPosition.x), height: CGFloat(maxPosition.y - minPosition.y))
+        if localBoundingBox.isEmpty {
+            return
+        }
+        
+        var transformMatrix = simd_float4x4(
+            SIMD4<Float>(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14)),
+            SIMD4<Float>(Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24)),
+            SIMD4<Float>(Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34)),
+            SIMD4<Float>(Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
+        )
+        
+        let identityTransform = CATransform3DIdentity
+        var identityTransformMatrix = SIMD16<Float>(
+            Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
+            Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
+            Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
+            Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
+        )
+        
+        let transform = CATransform3DGetAffineTransform(self.transform)
+        let boundingBox = localBoundingBox.applying(transform)
+        let baseVertex = boundingBox.origin.applying(transform.inverted())
+        
+        encoder.setRenderPipelineState(context.clearPipelineState)
+        
+        var quadVertices: [SIMD4<Float>] = [
+            SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
+            SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
+            SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
+            
+            SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
+            SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
+            SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
+        ]
+        
+        encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
+        encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
+        encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
+        
+        encoder.setRenderPipelineState(context.shapePipelineState)
+        encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
+        var baseVertexData = SIMD2<Float>(Float(baseVertex.x), Float(baseVertex.y))
+        encoder.setVertexBytes(&baseVertexData, length: 4 * 2, index: 2)
+        
+        var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
+        encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
+        
+        for subpath in self.subpaths {
+            encoder.setVertexBuffer(buffer, offset: subpath.bufferOffset, index: 0)
+            encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: (subpath.vertexCount - 1) * 3)
+        }
+        
+        encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
+        encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
+        
+        switch self.shading {
+        case let .color(color):
+            encoder.setRenderPipelineState(context.mergeColorFillPipelineState)
+            
+            var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
+            encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
+        case let .gradient(gradient):
+            switch gradient.gradientType {
+            case .linear:
+                encoder.setRenderPipelineState(context.mergeLinearGradientFillPipelineState)
+            case .radial:
+                encoder.setRenderPipelineState(context.mergeRadialGradientFillPipelineState)
+            }
+            
+            var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
+            encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
+            
+            let colorStopSize = 4 * 4 + 4
+            var colorStopsData = Data(count: colorStopSize * gradient.colorStops.count)
+            colorStopsData.withUnsafeMutableBytes { buffer in
+                let bytes = buffer.baseAddress!.assumingMemoryBound(to: Float.self)
+                for i in 0 ..< gradient.colorStops.count {
+                    let colorStop = gradient.colorStops[i]
+                    bytes[i * 5 + 0] = Float(colorStop.color.r)
+                    bytes[i * 5 + 1] = Float(colorStop.color.g)
+                    bytes[i * 5 + 2] = Float(colorStop.color.b)
+                    bytes[i * 5 + 3] = Float(colorStop.color.a)
+                    bytes[i * 5 + 4] = colorStop.location
+                }
+                encoder.setFragmentBytes(buffer.baseAddress!, length: buffer.count, index: 0)
+            }
+            
+            var numColorStops: UInt32 = UInt32(gradient.colorStops.count)
+            encoder.setFragmentBytes(&numColorStops, length: 4, index: 2)
+            
+            var startPosition = transformMatrix * SIMD4<Float>(gradient.start.x, gradient.start.y, 0.0, 1.0)
+            encoder.setFragmentBytes(&startPosition, length: 4 * 2, index: 3)
+            var endPosition = transformMatrix * SIMD4<Float>(gradient.end.x, gradient.end.y, 0.0, 1.0)
+            encoder.setFragmentBytes(&endPosition, length: 4 * 2, index: 4)
+        }
+        
+        encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
+    }
+}
+
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift
new file mode 100644
index 0000000000..ce9accfe1f
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift
@@ -0,0 +1,425 @@
+import Foundation
+import MetalKit
+import LottieCpp
+    
+func evaluateBezier(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float) -> SIMD2<Float> {
+    let t2 = t * t
+    let t3 = t * t * t
+
+    let A = (3 * t2 - 3 * t3)
+    let B = (3 * t3 - 6 * t2 + 3 * t)
+    let C = (3 * t2 - t3 - 3 * t + 1)
+
+    let value = t3 * p3 + A * p2 + B * p1 + C * p0
+    return value
+}
+    
+func evaluateBezier(p0: Float, p1: Float, p2: Float, p3: Float, t: Float) -> Float {
+    let oneMinusT = 1.0 - t
+    
+    let value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3
+    return value
+}
+    
+func solveQuadratic(p0: Float, p1: Float, p2: Float, p3: Float) -> (Float, Float) {
+    let i = p1 - p0
+    let j = p2 - p1
+    let k = p3 - p2
+
+    let a = (3 * i) - (6 * j) + (3 * k)
+    let b = (6 * j) - (6 * i)
+    let c = (3 * i)
+
+    let sqrtPart = (b * b) - (4 * a * c)
+    let hasSolution = sqrtPart >= 0
+    if !hasSolution {
+        return (.nan, .nan)
+    }
+
+    let t1 = (-b + sqrt(sqrtPart)) / (2 * a)
+    let t2 = (-b - sqrt(sqrtPart)) / (2 * a)
+
+    var s1: Float = .nan
+    var s2: Float = .nan
+
+    if t1 >= 0.0 && t1 <= 1.0 {
+        s1 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t1)
+    }
+
+    if t2 >= 0.0 && t2 <= 1.0 {
+        s2 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t2)
+    }
+
+    return (s1, s2)
+}
+    
+func bezierBounds(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>) -> (minPosition: SIMD2<Float>, maxPosition: SIMD2<Float>) {
+    let (solX1, solX2) = solveQuadratic(p0: p0.x, p1: p1.x, p2: p2.x, p3: p3.x)
+    let (solY1, solY2) = solveQuadratic(p0: p0.y, p1: p1.y, p2: p2.y, p3: p3.y)
+    
+    var minX = min(p0.x, p3.x)
+    var maxX = max(p0.x, p3.x)
+    
+    if !solX1.isNaN {
+        minX = min(minX, solX1)
+        maxX = max(maxX, solX1)
+    }
+    
+    if !solX2.isNaN {
+        minX = min(minX, solX2)
+        maxX = max(maxX, solX2)
+    }
+    
+    var minY = min(p0.y, p3.y)
+    var maxY = max(p0.y, p3.y)
+    
+    if !solY1.isNaN {
+        minY = min(minY, solY1)
+        maxY = max(maxY, solY1)
+    }
+    
+    if !solY2.isNaN {
+        minY = min(minY, solY2)
+        maxY = max(maxY, solY2)
+    }
+    
+    return (SIMD2<Float>(minX, minY), SIMD2<Float>(maxX, maxY))
+}
+
+final class PathRenderSubpathStrokeState {
+    struct TerminalState {
+        var bufferOffset: Int
+        var segmentCount: Int
+    }
+    
+    enum UnresolvedPosition {
+        case position(SIMD2<Float>)
+        case curve(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float)
+        
+        func resolve() -> SIMD2<Float> {
+            switch self {
+            case let .position(value):
+                return value
+            case let .curve(p0, p1, p2, p3, t):
+                return evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t)
+            }
+        }
+    }
+    
+    private let buffer: PathRenderBuffer
+    private let bezierDataBuffer: PathRenderBuffer
+    let bufferOffset: Int
+    private(set) var vertexCount: Int = 0
+    
+    private(set) var terminalState: TerminalState?
+    
+    private(set) var curveJoinVertexRanges: [Range<Int>] = []
+    
+    private var firstPosition: SIMD2<Float>
+    private var secondPosition: UnresolvedPosition
+    private var thirdPosition: UnresolvedPosition
+    
+    private var lastPosition: SIMD2<Float>
+    private var lastMinus1Position: UnresolvedPosition
+    private var lastMinus2Position: UnresolvedPosition
+    
+    private(set) var isClosed: Bool = false
+    private(set) var isCompleted: Bool = false
+    
+    init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
+        self.buffer = buffer
+        self.bezierDataBuffer = bezierDataBuffer
+        self.bufferOffset = buffer.length
+        
+        self.firstPosition = point
+        self.secondPosition = .position(point)
+        self.thirdPosition = .position(point)
+        self.lastPosition = point
+        self.lastMinus1Position = .position(point)
+        self.lastMinus2Position = .position(point)
+        
+        self.add(point: point)
+    }
+    
+    func add(point: SIMD2<Float>) {
+        self.buffer.append(float2: point)
+        
+        self.lastMinus2Position = self.lastMinus1Position
+        self.lastMinus1Position = .position(self.lastPosition)
+        self.lastPosition = point
+        
+        self.vertexCount += 1
+        if self.vertexCount == 2 {
+            self.secondPosition = .position(point)
+        } else if self.vertexCount == 3 {
+            self.thirdPosition = .position(point)
+        }
+    }
+    
+    func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
+        let stepCount = 8
+        self.bezierDataBuffer.appendBezierData(
+            bufferOffset: self.buffer.length / 4,
+            start: self.lastPosition,
+            end: point,
+            cp1: cp1,
+            cp2: cp2,
+            offset: 0.0
+        )
+        self.buffer.appendZero(count: 4 * 2 * stepCount)
+        
+        if self.vertexCount == 1 {
+            self.secondPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(1) / Float(stepCount))
+            self.thirdPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(2) / Float(stepCount))
+        }
+        
+        self.vertexCount += stepCount
+        
+        self.lastMinus2Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 2) / Float(stepCount))
+        self.lastMinus1Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 1) / Float(stepCount))
+        self.lastPosition = point
+    }
+    
+    func close() {
+        if self.isClosed {
+            assert(false)
+        } else {
+            self.isClosed = true
+            
+            if self.lastPosition != self.firstPosition {
+                self.add(point: self.firstPosition)
+            }
+        }
+    }
+    
+    func complete() {
+        if self.isCompleted {
+            assert(false)
+        } else {
+            if self.isClosed {
+                if self.vertexCount >= 3 {
+                    self.buffer.append(float2: self.secondPosition.resolve())
+                    self.buffer.append(float2: self.thirdPosition.resolve())
+                    self.vertexCount += 2
+                }
+            } else {
+                if self.vertexCount == 2 {
+                    let terminalBufferOffset = self.buffer.length
+                    
+                    let resolvedSecond = self.secondPosition.resolve()
+                    self.buffer.append(float2: self.firstPosition)
+                    self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
+                    self.buffer.append(float2: resolvedSecond)
+                    
+                    self.buffer.append(float2: resolvedSecond)
+                    self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
+                    self.buffer.append(float2: self.firstPosition)
+                    
+                    self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
+                } else if self.vertexCount >= 3 {
+                    let terminalBufferOffset = self.buffer.length
+                    
+                    self.buffer.append(float2: self.firstPosition)
+                    self.buffer.append(float2: self.secondPosition.resolve())
+                    self.buffer.append(float2: self.thirdPosition.resolve())
+                    
+                    self.buffer.append(float2: self.lastPosition)
+                    self.buffer.append(float2: self.lastMinus1Position.resolve())
+                    self.buffer.append(float2: self.lastMinus2Position.resolve())
+                    
+                    self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
+                }
+            }
+        }
+    }
+}
+
+final class PathRenderStrokeState {
+    private let buffer: PathRenderBuffer
+    private let bezierDataBuffer: PathRenderBuffer
+    private let lineWidth: Float
+    private let lineJoin: CGLineJoin
+    private let lineCap: CGLineCap
+    private let miterLimit: Float
+    private let color: LottieColor
+    private let transform: CATransform3D
+    
+    private var currentSubpath: PathRenderSubpathStrokeState?
+    private(set) var subpaths: [PathRenderSubpathStrokeState] = []
+    
+    init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, lineWidth: Float, lineJoin: CGLineJoin, lineCap: CGLineCap, miterLimit: Float, color: LottieColor, transform: CATransform3D) {
+        self.buffer = buffer
+        self.bezierDataBuffer = bezierDataBuffer
+        self.lineWidth = lineWidth
+        self.lineJoin = lineJoin
+        self.lineCap = lineCap
+        self.miterLimit = miterLimit
+        self.color = color
+        self.transform = transform
+    }
+    
+    func begin(point: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.complete()
+            self.subpaths.append(currentSubpath)
+            self.currentSubpath = nil
+        }
+        
+        self.currentSubpath = PathRenderSubpathStrokeState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
+    }
+    
+    func addLine(to point: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.add(point: point)
+        }
+    }
+    
+    func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
+        }
+    }
+    
+    func close() {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.close()
+            currentSubpath.complete()
+            self.subpaths.append(currentSubpath)
+            self.currentSubpath = nil
+        }
+    }
+    
+    func complete() {
+        if let currentSubpath = self.currentSubpath {
+            currentSubpath.complete()
+            self.subpaths.append(currentSubpath)
+            self.currentSubpath = nil
+        }
+    }
+    
+    func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
+        if self.subpaths.isEmpty {
+            return
+        }
+        
+        encoder.setVertexBuffer(buffer, offset: 0, index: 0)
+        
+        var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
+        encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
+        
+        var transformMatrix = SIMD16<Float>(
+            Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14),
+            Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24),
+            Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34),
+            Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44)
+        )
+        encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
+        
+        let capRes2: Float
+        switch self.lineCap {
+        case .butt:
+            capRes2 = 2.0
+        case .square:
+            capRes2 = 6.0
+        case .round:
+            capRes2 = 24.0
+        @unknown default:
+            capRes2 = 2.0
+        }
+        let joinRes2: Float = self.lineJoin == .round ? 16.0 : 2.0
+        
+        func computeCount(isEndpoints: Bool, insertCaps: Bool) -> SIMD2<Float> {
+            if insertCaps {
+                if isEndpoints {
+                    return SIMD2<Float>(capRes2, max(capRes2, joinRes2))
+                } else {
+                    return SIMD2<Float>(max(capRes2, joinRes2), max(capRes2, joinRes2))
+                }
+            } else {
+                if isEndpoints {
+                    return SIMD2<Float>(capRes2, joinRes2)
+                } else {
+                    return SIMD2<Float>(joinRes2, joinRes2)
+                }
+            }
+        }
+        
+        var hasTerminalStates = false
+        
+        for subpath in self.subpaths {
+            let segmentCount = subpath.vertexCount - 1
+            if segmentCount <= 0 {
+                continue
+            }
+            
+            if subpath.vertexCount >= 4 {
+                encoder.setRenderPipelineState(context.strokeInnerPipelineState)
+                
+                encoder.setVertexBufferOffset(subpath.bufferOffset, index: 0)
+                
+                var vertCnt2 = computeCount(isEndpoints: false, insertCaps: false)
+                encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
+                
+                var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
+                encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
+                
+                var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
+                encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
+                
+                var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
+                encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
+                
+                var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
+                encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
+                
+                var lineWidthValue: Float = self.lineWidth * 0.5
+                encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
+                
+                let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
+                encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: subpath.vertexCount - 4 + 1, baseInstance: 0)
+            }
+            
+            if subpath.terminalState != nil {
+                hasTerminalStates = true
+            }
+        }
+        
+        if hasTerminalStates {
+            encoder.setRenderPipelineState(context.strokeTerminalPipelineState)
+            
+            for subpath in self.subpaths {
+                let segmentCount = subpath.vertexCount - 1
+                if segmentCount <= 0 {
+                    continue
+                }
+                
+                if !subpath.isClosed {
+                    if let terminalState = subpath.terminalState {
+                        encoder.setVertexBufferOffset(terminalState.bufferOffset, index: 0)
+                        
+                        var vertCnt2 = computeCount(isEndpoints: true, insertCaps: false)
+                        encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
+                        
+                        var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
+                        encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
+                        
+                        var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
+                        encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
+                        
+                        var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
+                        encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
+                        
+                        var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
+                        encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
+                        
+                        var lineWidthValue: Float = self.lineWidth * 0.5
+                        encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
+                        
+                        let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
+                        encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: terminalState.segmentCount, baseInstance: 0)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift
new file mode 100644
index 0000000000..ee54cc3baa
--- /dev/null
+++ b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift
@@ -0,0 +1,485 @@
+import Foundation
+import LottieCpp
+
+final class WriteBuffer {
+    private(set) var data: Data
+    private var capacity: Int
+    var length: Int
+    
+    init() {
+        self.capacity = 1024
+        self.data = Data(count: self.capacity)
+        self.length = 0
+    }
+    
+    func trim() {
+        self.data.count = self.length
+        self.capacity = self.data.count
+    }
+    
+    func write(bytes: UnsafeRawBufferPointer) {
+        if self.data.count < self.length + bytes.count {
+            self.data.count = self.data.count * 2
+        }
+        self.data.withUnsafeMutableBytes { buffer -> Void in
+            memcpy(buffer.baseAddress!.advanced(by: self.length), bytes.baseAddress!, bytes.count)
+        }
+        self.length += bytes.count
+    }
+    
+    func write(uInt32 value: UInt32) {
+        var value = value
+        withUnsafeBytes(of: &value, { bytes in
+            self.write(bytes: bytes)
+        })
+    }
+    
+    func write(uInt16 value: UInt16) {
+        var value = value
+        withUnsafeBytes(of: &value, { bytes in
+            self.write(bytes: bytes)
+        })
+    }
+    
+    func write(uInt8 value: UInt8) {
+        var value = value
+        withUnsafeBytes(of: &value, { bytes in
+            self.write(bytes: bytes)
+        })
+    }
+    
+    func write(float value: Float) {
+        var value = value
+        withUnsafeBytes(of: &value, { bytes in
+            self.write(bytes: bytes)
+        })
+    }
+    
+    func write(point: CGPoint) {
+        self.write(float: Float(point.x))
+        self.write(float: Float(point.y))
+    }
+    
+    func write(size: CGSize) {
+        self.write(float: Float(size.width))
+        self.write(float: Float(size.height))
+    }
+    
+    func write(rect: CGRect) {
+        self.write(point: rect.origin)
+        self.write(size: rect.size)
+    }
+    
+    func write(transform: CATransform3D) {
+        self.write(float: Float(transform.m11))
+        self.write(float: Float(transform.m12))
+        self.write(float: Float(transform.m13))
+        self.write(float: Float(transform.m14))
+        self.write(float: Float(transform.m21))
+        self.write(float: Float(transform.m22))
+        self.write(float: Float(transform.m23))
+        self.write(float: Float(transform.m24))
+        self.write(float: Float(transform.m31))
+        self.write(float: Float(transform.m32))
+        self.write(float: Float(transform.m33))
+        self.write(float: Float(transform.m34))
+        self.write(float: Float(transform.m41))
+        self.write(float: Float(transform.m42))
+        self.write(float: Float(transform.m43))
+        self.write(float: Float(transform.m44))
+    }
+}
+
+final class ReadBuffer {
+    private let data: Data
+    private var offset: Int
+    
+    init(data: Data) {
+        self.data = data
+        self.offset = 0
+    }
+    
+    func read(bytes: UnsafeMutableRawBufferPointer) {
+        if self.offset + bytes.count <= self.data.count {
+            self.data.withUnsafeBytes { buffer -> Void in
+                memcpy(bytes.baseAddress!, buffer.baseAddress!.advanced(by: self.offset), bytes.count)
+            }
+            self.offset += bytes.count
+        } else {
+            preconditionFailure()
+        }
+    }
+    
+    func readUInt32() -> UInt32 {
+        var value: UInt32 = 0
+        withUnsafeMutableBytes(of: &value, { bytes in
+            self.read(bytes: bytes)
+        })
+        return value
+    }
+    
+    func readUInt16() -> UInt16 {
+        var value: UInt16 = 0
+        withUnsafeMutableBytes(of: &value, { bytes in
+            self.read(bytes: bytes)
+        })
+        return value
+    }
+    
+    func readUInt8() -> UInt8 {
+        var value: UInt8 = 0
+        withUnsafeMutableBytes(of: &value, { bytes in
+            self.read(bytes: bytes)
+        })
+        return value
+    }
+    
+    func readFloat() -> Float {
+        var value: Float = 0
+        withUnsafeMutableBytes(of: &value, { bytes in
+            self.read(bytes: bytes)
+        })
+        return value
+    }
+    
+    func readPoint() -> CGPoint {
+        return CGPoint(x: CGFloat(self.readFloat()), y: CGFloat(self.readFloat()))
+    }
+    
+    func readSize() -> CGSize {
+        return CGSize(width: CGFloat(self.readFloat()), height: CGFloat(self.readFloat()))
+    }
+    
+    func readRect() -> CGRect {
+        return CGRect(origin: self.readPoint(), size: self.readSize())
+    }
+    
+    func readTransform() -> CATransform3D {
+        return CATransform3D(
+            m11: CGFloat(self.readFloat()),
+            m12: CGFloat(self.readFloat()),
+            m13: CGFloat(self.readFloat()),
+            m14: CGFloat(self.readFloat()),
+            m21: CGFloat(self.readFloat()),
+            m22: CGFloat(self.readFloat()),
+            m23: CGFloat(self.readFloat()),
+            m24: CGFloat(self.readFloat()),
+            m31: CGFloat(self.readFloat()),
+            m32: CGFloat(self.readFloat()),
+            m33: CGFloat(self.readFloat()),
+            m34: CGFloat(self.readFloat()),
+            m41: CGFloat(self.readFloat()),
+            m42: CGFloat(self.readFloat()),
+            m43: CGFloat(self.readFloat()),
+            m44: CGFloat(self.readFloat())
+        )
+    }
+}
+
+private extension LottieColor {
+    init(argb: UInt32) {
+        self.init(r: CGFloat((argb >> 16) & 0xff) / 255.0, g: CGFloat((argb >> 8) & 0xff) / 255.0, b: CGFloat(argb & 0xff) / 255.0, a: CGFloat((argb >> 24) & 0xff) / 255.0)
+    }
+    
+    var argb: UInt32 {
+        return (UInt32(self.a * 255.0) << 24) | (UInt32(max(0.0, self.r) * 255.0) << 16) | (UInt32(max(0.0, self.g) * 255.0) << 8) | (UInt32(max(0.0, self.b) * 255.0))
+    }
+}
+    
+private struct NodeFlags: OptionSet {
+    var rawValue: UInt8
+    
+    init(rawValue: UInt8) {
+        self.rawValue = rawValue
+    }
+    
+    static let masksToBounds = NodeFlags(rawValue: 1 << 0)
+    static let isHidden = NodeFlags(rawValue: 1 << 1)
+    static let hasSimpleContents = NodeFlags(rawValue: 1 << 2)
+    static let isInvertedMatte = NodeFlags(rawValue: 1 << 3)
+    
+    static let hasRenderContent = NodeFlags(rawValue: 1 << 4)
+    static let hasSubnodes = NodeFlags(rawValue: 1 << 5)
+    static let hasMask = NodeFlags(rawValue: 1 << 6)
+}
+    
+private struct LottieContentFlags: OptionSet {
+    var rawValue: UInt8
+    
+    init(rawValue: UInt8) {
+        self.rawValue = rawValue
+    }
+    
+    static let hasStroke = LottieContentFlags(rawValue: 1 << 0)
+    static let hasFill = LottieContentFlags(rawValue: 1 << 1)
+}
+    
+func serializePath(buffer: WriteBuffer, path: LottiePath) {
+    let lengthOffset = buffer.length
+    buffer.write(uInt32: 0)
+        
+    path.enumerateItems { pathItem in
+        switch pathItem.pointee.type {
+        case .moveTo:
+            let point = pathItem.pointee.points.0
+            buffer.write(uInt8: 0)
+            buffer.write(point: point)
+        case .lineTo:
+            let point = pathItem.pointee.points.0
+            buffer.write(uInt8: 1)
+            buffer.write(point: point)
+        case .curveTo:
+            let cp1 = pathItem.pointee.points.0
+            let cp2 = pathItem.pointee.points.1
+            let point = pathItem.pointee.points.2
+            
+            buffer.write(uInt8: 2)
+            buffer.write(point: cp1)
+            buffer.write(point: cp2)
+            buffer.write(point: point)
+        case .close:
+            buffer.write(uInt8: 3)
+        @unknown default:
+            break
+        }
+    }
+    
+    let dataLength = buffer.length - lengthOffset - 4
+    
+    let previousLength = buffer.length
+    buffer.length = lengthOffset
+    buffer.write(uInt32: UInt32(dataLength))
+    buffer.length = previousLength
+}
+    
+func deserializePath(buffer: ReadBuffer) -> LottiePath {
+    let itemDataLength = Int(buffer.readUInt32())
+    var itemData = Data(count: itemDataLength)
+    itemData.withUnsafeMutableBytes { bytes in
+        buffer.read(bytes: bytes)
+    }
+    
+    return LottiePath(customData: itemData)
+}
+    
+func serializeContentShading(buffer: WriteBuffer, shading: LottieRenderContentShading) {
+    if let shading = shading as? LottieRenderContentSolidShading {
+        buffer.write(uInt8: 0)
+        buffer.write(uInt32: shading.color.argb)
+        buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0)))
+    } else if let shading = shading as? LottieRenderContentGradientShading {
+        buffer.write(uInt8: 1)
+        buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0)))
+        buffer.write(uInt8: UInt8(shading.gradientType.rawValue))
+        let colorStopCount = min(shading.colorStops.count, 255)
+        buffer.write(uInt8: UInt8(colorStopCount))
+        for i in 0 ..< colorStopCount {
+            buffer.write(uInt32: shading.colorStops[i].color.argb)
+            buffer.write(float: Float(shading.colorStops[i].location))
+        }
+        buffer.write(point: shading.start)
+        buffer.write(point: shading.end)
+    } else {
+        buffer.write(uInt8: 0)
+        buffer.write(uInt8: UInt8(clamping: Int(1.0 * 255.0)))
+    }
+}
+    
+func deserializeContentShading(buffer: ReadBuffer) -> LottieRenderContentShading {
+    switch buffer.readUInt8() {
+    case 0:
+        return LottieRenderContentSolidShading(
+            color: LottieColor(argb: buffer.readUInt32()),
+            opacity: CGFloat(buffer.readUInt8()) / 255.0
+        )
+    case 1:
+        let opacity = CGFloat(buffer.readUInt8()) / 255.0
+        let gradientType = LottieGradientType(rawValue: UInt(buffer.readUInt8()))!
+        
+        var colorStops: [LottieColorStop] = []
+        let colorStopCount = Int(buffer.readUInt8())
+        for _ in 0 ..< colorStopCount {
+            colorStops.append(LottieColorStop(
+                color: LottieColor(argb: buffer.readUInt32()),
+                location: CGFloat(buffer.readFloat())
+            ))
+        }
+        
+        let start = buffer.readPoint()
+        let end = buffer.readPoint()
+        
+        return LottieRenderContentGradientShading(
+            opacity: opacity,
+            gradientType: gradientType,
+            colorStops: colorStops,
+            start: start,
+            end: end
+        )
+    default:
+        preconditionFailure()
+    }
+}
+    
+func serializeStroke(buffer: WriteBuffer, stroke: LottieRenderContentStroke) {
+    serializeContentShading(buffer: buffer, shading: stroke.shading)
+    buffer.write(float: Float(stroke.lineWidth))
+    buffer.write(uInt8: UInt8(stroke.lineJoin.rawValue))
+    buffer.write(uInt8: UInt8(stroke.lineCap.rawValue))
+    buffer.write(float: Float(stroke.miterLimit))
+}
+    
+func deserializeStroke(buffer: ReadBuffer) -> LottieRenderContentStroke {
+    return LottieRenderContentStroke(
+        shading: deserializeContentShading(buffer: buffer),
+        lineWidth: CGFloat(buffer.readFloat()),
+        lineJoin: CGLineJoin(rawValue: Int32(buffer.readUInt8()))!,
+        lineCap: CGLineCap(rawValue: Int32(buffer.readUInt8()))!,
+        miterLimit: CGFloat(buffer.readFloat()),
+        dashPhase: 0.0,
+        dashPattern: nil
+    )
+}
+    
+func serializeFill(buffer: WriteBuffer, fill: LottieRenderContentFill) {
+    serializeContentShading(buffer: buffer, shading: fill.shading)
+    buffer.write(uInt8: UInt8(fill.fillRule.rawValue))
+}
+    
+func deserializeFill(buffer: ReadBuffer) -> LottieRenderContentFill {
+    return LottieRenderContentFill(
+        shading: deserializeContentShading(buffer: buffer),
+        fillRule: LottieFillRule(rawValue: UInt(buffer.readUInt8()))!
+    )
+}
+    
+func serializeRenderContent(buffer: WriteBuffer, renderContent: LottieRenderContent) {
+    var flags: LottieContentFlags = []
+    if renderContent.stroke != nil {
+        flags.insert(.hasStroke)
+    }
+    if renderContent.fill != nil {
+        flags.insert(.hasFill)
+    }
+    buffer.write(uInt8: flags.rawValue)
+    
+    serializePath(buffer: buffer, path: renderContent.path)
+    if let stroke = renderContent.stroke {
+        serializeStroke(buffer: buffer, stroke: stroke)
+    }
+    if let fill = renderContent.fill {
+        serializeFill(buffer: buffer, fill: fill)
+    }
+}
+    
+func deserializeRenderContent(buffer: ReadBuffer) -> LottieRenderContent {
+    let flags = LottieContentFlags(rawValue: buffer.readUInt8())
+    
+    let path = deserializePath(buffer: buffer)
+    
+    var stroke: LottieRenderContentStroke?
+    if flags.contains(.hasStroke) {
+        stroke = deserializeStroke(buffer: buffer)
+    }
+    
+    var fill: LottieRenderContentFill?
+    if flags.contains(.hasFill) {
+        fill = deserializeFill(buffer: buffer)
+    }
+    
+    return LottieRenderContent(
+        path: path,
+        stroke: stroke,
+        fill: fill
+    )
+}
+
+func serializeNode(buffer: WriteBuffer, node: LottieRenderNode) {
+    var flags: NodeFlags = []
+    if node.masksToBounds {
+        flags.insert(.masksToBounds)
+    }
+    if node.isHidden {
+        flags.insert(.isHidden)
+    }
+    if node.hasSimpleContents {
+        flags.insert(.hasSimpleContents)
+    }
+    if node.isInvertedMatte {
+        flags.insert(.isInvertedMatte)
+    }
+    if node.renderContent != nil {
+        flags.insert(.hasRenderContent)
+    }
+    if !node.subnodes.isEmpty {
+        flags.insert(.hasSubnodes)
+    }
+    if node.mask != nil {
+        flags.insert(.hasMask)
+    }
+    
+    buffer.write(uInt8: flags.rawValue)
+    
+    buffer.write(point: node.position)
+    buffer.write(rect: node.bounds)
+    buffer.write(transform: node.transform)
+    buffer.write(uInt8: UInt8(clamping: Int(node.opacity * 255.0)))
+    buffer.write(rect: node.globalRect)
+    buffer.write(transform: node.globalTransform)
+    
+    if let renderContent = node.renderContent {
+        serializeRenderContent(buffer: buffer, renderContent: renderContent)
+    }
+    if !node.subnodes.isEmpty {
+        let count = min(node.subnodes.count, 4095)
+        buffer.write(uInt16: UInt16(count))
+        for i in 0 ..< count {
+            serializeNode(buffer: buffer, node: node.subnodes[i])
+        }
+    }
+    if let mask = node.mask {
+        serializeNode(buffer: buffer, node: mask)
+    }
+}
+    
+func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode {
+    let flags = NodeFlags(rawValue: buffer.readUInt8())
+    
+    let position = buffer.readPoint()
+    let bounds = buffer.readRect()
+    let transform = buffer.readTransform()
+    let opacity = CGFloat(buffer.readUInt8()) / 255.0
+    let globalRect = buffer.readRect()
+    let globalTransform = buffer.readTransform()
+    
+    var renderContent: LottieRenderContent?
+    if flags.contains(.hasRenderContent) {
+        renderContent = deserializeRenderContent(buffer: buffer)
+    }
+    var subnodes: [LottieRenderNode] = []
+    if flags.contains(.hasSubnodes) {
+        let count = Int(buffer.readUInt16())
+        for _ in 0 ..< count {
+            subnodes.append(deserializeNode(buffer: buffer))
+        }
+    }
+    var mask: LottieRenderNode?
+    if flags.contains(.hasMask) {
+        mask = deserializeNode(buffer: buffer)
+    }
+    
+    return LottieRenderNode(
+        position: position,
+        bounds: bounds,
+        transform: transform,
+        opacity: opacity,
+        masksToBounds: flags.contains(.masksToBounds),
+        isHidden: flags.contains(.isHidden),
+        globalRect: globalRect,
+        globalTransform: globalTransform,
+        renderContent: renderContent,
+        hasSimpleContents: flags.contains(.hasSimpleContents),
+        isInvertedMatte: flags.contains(.isInvertedMatte),
+        subnodes: subnodes,
+        mask: mask
+    )
+}