diff --git a/patches/api/Expanded-Art-API.patch b/patches/api/Expanded-Art-API.patch
new file mode 100644
index 0000000000..39625adfbf
--- /dev/null
+++ b/patches/api/Expanded-Art-API.patch
@@ -0,0 +1,40 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kokiriglade <60290002+celerry@users.noreply.github.com>
+Date: Sat, 23 Nov 2024 18:08:13 +0000
+Subject: [PATCH] Expanded Art API
+
+
+diff --git a/src/main/java/org/bukkit/Art.java b/src/main/java/org/bukkit/Art.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/Art.java
++++ b/src/main/java/org/bukkit/Art.java
+@@ -0,0 +0,0 @@ public interface Art extends OldEnum<Art>, Keyed {
+     @NotNull NamespacedKey getKey();
+     // Paper end - deprecate getKey
+ 
++    // Paper start - name and author components, assetId key
++    /**
++     * Get the painting's title.
++     *
++     * @return the title
++     */
++    net.kyori.adventure.text.@Nullable Component title();
++
++    /**
++     * Get the painting's author.
++     *
++     * @return the author
++     */
++    net.kyori.adventure.text.@Nullable Component author();
++
++    /**
++     * Get the painting's asset id
++     *
++     * @return the asset id
++     */
++    net.kyori.adventure.key.@NotNull Key assetId();
++    // Paper end - name and author components, assetId key
++
+     /**
+      * Get a painting by its numeric ID
+      *
diff --git a/patches/api/Introduce-registry-entry-and-builders.patch b/patches/api/Introduce-registry-entry-and-builders.patch
index 6f91370b9f..e345a913fc 100644
--- a/patches/api/Introduce-registry-entry-and-builders.patch
+++ b/patches/api/Introduce-registry-entry-and-builders.patch
@@ -3,6 +3,7 @@ From: Bjarne Koll <git@lynxplay.dev>
 Date: Thu, 13 Jun 2024 22:35:05 +0200
 Subject: [PATCH] Introduce registry entry and builders
 
+Co-authored-by: kokiriglade <git@kokirigla.de>
 
 diff --git a/src/main/java/io/papermc/paper/registry/RegistryKey.java b/src/main/java/io/papermc/paper/registry/RegistryKey.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
@@ -414,6 +415,147 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        Builder range(@Range(from = 0, to = Integer.MAX_VALUE) int range);
 +    }
 +}
+diff --git a/src/main/java/io/papermc/paper/registry/data/PaintingVariantRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaintingVariantRegistryEntry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/data/PaintingVariantRegistryEntry.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.registry.data;
++
++import io.papermc.paper.registry.RegistryBuilder;
++import java.util.Optional;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.text.Component;
++import org.bukkit.Art;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.Range;
++import org.jspecify.annotations.NullMarked;
++import org.jspecify.annotations.Nullable;
++
++/**
++ * A data-centric version-specific registry entry for the {@link Art} type.
++ */
++@ApiStatus.Experimental
++@NullMarked
++@ApiStatus.NonExtendable
++public interface PaintingVariantRegistryEntry {
++
++    /**
++     * Provides the width of this variant in blocks.
++     *
++     * @return the width
++     * @see Art#getBlockWidth()
++     */
++    @Range(from = 1, to = 16)
++    int width();
++
++    /**
++     * Provides the height of this variant in blocks.
++     *
++     * @return the height
++     * @see Art#getBlockHeight()
++     */
++    @Range(from = 1, to = 16)
++    int height();
++
++    /**
++     * Provides the title of the painting visible in the creative inventory.
++     *
++     * @return the title
++     * @see Art#title()
++     */
++    @Nullable Component title();
++
++    /**
++     * Provides the author of the painting visible in the creative inventory.
++     *
++     * @return the author
++     * @see Art#author()
++     */
++    @Nullable Component author();
++
++    /**
++     * Provides the assetId of the variant, which is the location of the sprite to use.
++     *
++     * @return the asset id
++     * @see Art#assetId()
++     */
++    Key assetId();
++
++    /**
++     * A mutable builder for the {@link PaintingVariantRegistryEntry} plugins may change in applicable registry events.
++     * <p>
++     * The following values are required for each builder:
++     * <ul>
++     *     <li>{@link #width(int)}</li>
++     *     <li>{@link #height(int)}</li>
++     *     <li>{@link #assetId(Key)}</li>
++     * </ul>
++     */
++    @ApiStatus.Experimental
++    @ApiStatus.NonExtendable
++    interface Builder extends PaintingVariantRegistryEntry, RegistryBuilder<Art> {
++
++        /**
++         * Sets the width of the painting in blocks.
++         *
++         * @param width the width in blocks
++         * @return this builder instance
++         * @see PaintingVariantRegistryEntry#width()
++         * @see Art#getBlockWidth()
++         */
++        @Contract(value = "_ -> this", mutates = "this")
++        Builder width(@Range(from = 0, to = 16) int width);
++
++        /**
++         * Sets the height of the painting in blocks.
++         *
++         * @param height the height in blocks
++         * @return this builder instance
++         * @see PaintingVariantRegistryEntry#height()
++         * @see Art#getBlockHeight()
++         */
++        @Contract(value = "_ -> this", mutates = "this")
++        Builder height(@Range(from = 0, to = 16) int height);
++
++        /**
++         * Sets the title of the painting.
++         *
++         * @param title the title
++         * @return this builder instance
++         * @see PaintingVariantRegistryEntry#title()
++         * @see Art#title()
++         */
++        @Contract(value = "_ -> this", mutates = "this")
++        Builder title(@Nullable Component title);
++
++        /**
++         * Sets the author of the painting.
++         *
++         * @param author the author
++         * @return this builder instance
++         * @see PaintingVariantRegistryEntry#author()
++         * @see Art#author()
++         */
++        @Contract(value = "_ -> this", mutates = "this")
++        Builder author(@Nullable Component author);
++
++        /**
++         * Sets the assetId of the variant, which is the location of the sprite to use.
++         *
++         * @param assetId the asset id
++         * @return this builder instance
++         * @see PaintingVariantRegistryEntry#assetId()
++         * @see Art#assetId()
++         */
++        @Contract(value = "_ -> this", mutates = "this")
++        Builder assetId(Key assetId);
++
++    }
++
++}
 diff --git a/src/main/java/io/papermc/paper/registry/data/package-info.java b/src/main/java/io/papermc/paper/registry/data/package-info.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -439,6 +581,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.registry.RegistryKey;
 +import io.papermc.paper.registry.data.EnchantmentRegistryEntry;
 +import io.papermc.paper.registry.data.GameEventRegistryEntry;
++import io.papermc.paper.registry.data.PaintingVariantRegistryEntry;
++import org.bukkit.Art;
 +import org.bukkit.GameEvent;
 +import org.bukkit.enchantments.Enchantment;
  import org.jetbrains.annotations.ApiStatus;
@@ -455,6 +599,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
 +    public static final RegistryEventProvider<GameEvent, GameEventRegistryEntry.Builder> GAME_EVENT = create(RegistryKey.GAME_EVENT);
 +    public static final RegistryEventProvider<Enchantment, EnchantmentRegistryEntry.Builder> ENCHANTMENT = create(RegistryKey.ENCHANTMENT);
++    public static final RegistryEventProvider<Art, PaintingVariantRegistryEntry.Builder> PAINTING_VARIANT = create(RegistryKey.PAINTING_VARIANT);
 +
      private RegistryEvents() {
      }
diff --git a/patches/server/Add-registry-entry-and-builders.patch b/patches/server/Add-registry-entry-and-builders.patch
index a607a80cc2..f3bae1a3ed 100644
--- a/patches/server/Add-registry-entry-and-builders.patch
+++ b/patches/server/Add-registry-entry-and-builders.patch
@@ -15,6 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import io.papermc.paper.adventure.PaperAdventure;
 +import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry;
 +import io.papermc.paper.registry.data.PaperGameEventRegistryEntry;
++import io.papermc.paper.registry.data.PaperPaintingVariantRegistryEntry;
  import io.papermc.paper.registry.entry.RegistryEntry;
  import io.papermc.paper.registry.tag.TagKey;
  import java.util.Collections;
@@ -35,7 +36,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            writable(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT, Enchantment.class, CraftEnchantment::new, PaperEnchantmentRegistryEntry.PaperBuilder::new).withSerializationUpdater(FieldRename.ENCHANTMENT_RENAME).delayed(),
              entry(Registries.JUKEBOX_SONG, RegistryKey.JUKEBOX_SONG, JukeboxSong.class, CraftJukeboxSong::new).delayed(),
              entry(Registries.BANNER_PATTERN, RegistryKey.BANNER_PATTERN, PatternType.class, CraftPatternType::new).delayed(),
-             entry(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT, Art.class, CraftArt::new).delayed(),
+-            entry(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT, Art.class, CraftArt::new).delayed(),
++            writable(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT, Art.class, CraftArt::new, PaperPaintingVariantRegistryEntry.PaperBuilder::new).delayed(),
+             entry(Registries.INSTRUMENT, RegistryKey.INSTRUMENT, MusicInstrument.class, CraftMusicInstrument::new).delayed(),
+ 
+             // api-only
 diff --git a/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -339,6 +344,132 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
+diff --git a/src/main/java/io/papermc/paper/registry/data/PaperPaintingVariantRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperPaintingVariantRegistryEntry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/data/PaperPaintingVariantRegistryEntry.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.registry.data;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.registry.PaperRegistryBuilder;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.data.util.Conversions;
++import java.util.Optional;
++import java.util.OptionalInt;
++import net.kyori.adventure.key.Key;
++import net.minecraft.network.chat.Component;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.world.entity.decoration.PaintingVariant;
++import org.bukkit.Art;
++import org.jetbrains.annotations.Range;
++import org.jspecify.annotations.NullMarked;
++import org.jspecify.annotations.Nullable;
++
++import static io.papermc.paper.registry.data.util.Checks.asArgument;
++import static io.papermc.paper.registry.data.util.Checks.asArgumentRange;
++import static io.papermc.paper.registry.data.util.Checks.asConfigured;
++
++@NullMarked
++public class PaperPaintingVariantRegistryEntry implements PaintingVariantRegistryEntry {
++
++    protected OptionalInt width = OptionalInt.empty();
++    protected OptionalInt height = OptionalInt.empty();
++    protected @Nullable Component title;
++    protected @Nullable Component author;
++    protected @Nullable ResourceLocation assetId;
++
++    protected final Conversions conversions;
++
++    public PaperPaintingVariantRegistryEntry(
++        final Conversions conversions,
++        final TypedKey<Art> ignoredKey,
++        final @Nullable PaintingVariant nms
++    ) {
++        this.conversions = conversions;
++        if(nms == null) return;
++
++        this.width = OptionalInt.of(nms.width());
++        this.height = OptionalInt.of(nms.height());
++        this.title = nms.title().orElse(null);
++        this.author = nms.title().orElse(null);
++        this.assetId = nms.assetId();
++    }
++
++    @Override
++    public @Range(from = 1, to = 16) int width() {
++        return asConfigured(this.width, "width");
++    }
++
++    @Override
++    public @Range(from = 1, to = 16) int height() {
++        return asConfigured(this.height, "height");
++    }
++
++    @Override
++    public net.kyori.adventure.text.@Nullable Component title() {
++        return this.title == null ? null : this.conversions.asAdventure(this.title);
++    }
++
++    @Override
++    public net.kyori.adventure.text.@Nullable Component author() {
++        return this.author == null ? null : this.conversions.asAdventure(this.author);
++    }
++
++    @Override
++    public Key assetId() {
++        return PaperAdventure.asAdventure(asConfigured(this.assetId, "assetId"));
++    }
++
++    public static final class PaperBuilder extends PaperPaintingVariantRegistryEntry implements PaintingVariantRegistryEntry.Builder, PaperRegistryBuilder<PaintingVariant, Art> {
++
++        public PaperBuilder(final Conversions conversions, final TypedKey<Art> key, final @Nullable PaintingVariant nms) {
++            super(conversions, key, nms);
++        }
++
++        @Override
++        public Builder width(@Range(from = 0, to = 16) final int width) {
++            this.width = OptionalInt.of(asArgumentRange(width, "width", 1, 16));
++            return this;
++        }
++
++        @Override
++        public Builder height(@Range(from = 0, to = 16) final int height) {
++            this.height = OptionalInt.of(asArgumentRange(height, "height", 1, 16));
++            return this;
++        }
++
++        @Override
++        public Builder title(final net.kyori.adventure.text.@Nullable Component title) {
++            this.title = this.conversions.asVanilla(title);
++            return this;
++        }
++
++        @Override
++        public Builder author(final net.kyori.adventure.text.@Nullable Component author) {
++            this.author = this.conversions.asVanilla(author);
++            return this;
++        }
++
++        @Override
++        public Builder assetId(final Key assetId) {
++            this.assetId = PaperAdventure.asVanilla(asArgument(assetId, "assetId"));
++            return this;
++        }
++
++        @Override
++        public PaintingVariant build() {
++            return new PaintingVariant(
++                this.width(),
++                this.height(),
++                asConfigured(this.assetId, "assetId"),
++                Optional.ofNullable(this.title),
++                Optional.ofNullable(this.author)
++            );
++        }
++    }
++}
 diff --git a/src/main/java/io/papermc/paper/registry/data/util/Checks.java b/src/main/java/io/papermc/paper/registry/data/util/Checks.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
diff --git a/patches/server/DataComponent-API.patch b/patches/server/DataComponent-API.patch
index ca10995882..96a4927eab 100644
--- a/patches/server/DataComponent-API.patch
+++ b/patches/server/DataComponent-API.patch
@@ -3600,7 +3600,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.datacomponent.PaperComponentType;
  import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry;
  import io.papermc.paper.registry.data.PaperGameEventRegistryEntry;
- import io.papermc.paper.registry.entry.RegistryEntry;
+ import io.papermc.paper.registry.data.PaperPaintingVariantRegistryEntry;
 @@ -0,0 +0,0 @@ public final class PaperRegistries {
              entry(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE, Attribute.class, CraftAttribute::new),
              entry(Registries.FLUID, RegistryKey.FLUID, Fluid.class, CraftFluid::new),
diff --git a/patches/server/Expanded-Art-API.patch b/patches/server/Expanded-Art-API.patch
new file mode 100644
index 0000000000..8ea7fe8921
--- /dev/null
+++ b/patches/server/Expanded-Art-API.patch
@@ -0,0 +1,53 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kokiriglade <60290002+celerry@users.noreply.github.com>
+Date: Sat, 23 Nov 2024 18:58:49 +0000
+Subject: [PATCH] Expanded Art API
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftArt.java b/src/main/java/org/bukkit/craftbukkit/CraftArt.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftArt.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftArt.java
+@@ -0,0 +0,0 @@ public class CraftArt implements Art, Handleable<PaintingVariant> {
+ 
+     private final NamespacedKey key;
+     private final PaintingVariant paintingVariant;
++    private final net.kyori.adventure.text.@org.jspecify.annotations.Nullable Component adventureTitle; // Paper - name and author components, assetId key
++    private final net.kyori.adventure.text.@org.jspecify.annotations.Nullable Component adventureAuthor; // Paper - name and author components, assetId key
++    private final net.kyori.adventure.key.@org.jspecify.annotations.NonNull Key adventureAssetId; // Paper - name and author components, assetId key
+     private final String name;
+     private final int ordinal;
+ 
+@@ -0,0 +0,0 @@ public class CraftArt implements Art, Handleable<PaintingVariant> {
+             this.name = key.toString();
+         }
+         this.ordinal = CraftArt.count++;
++        this.adventureTitle = paintingVariant.title().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); // Paper - name and author components, assetId key
++        this.adventureAuthor = paintingVariant.author().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); // Paper - name and author components, assetId key
++        this.adventureAssetId = io.papermc.paper.adventure.PaperAdventure.asAdventure(paintingVariant.assetId()); // Paper - name and author components, assetId key
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class CraftArt implements Art, Handleable<PaintingVariant> {
+         return this.paintingVariant.height();
+     }
+ 
++    // Paper start - name and author components, assetId key
++    @Override
++    public net.kyori.adventure.text.@org.jetbrains.annotations.Nullable Component title() {
++        return this.adventureTitle;
++    }
++
++    @Override
++    public net.kyori.adventure.text.@org.jetbrains.annotations.Nullable Component author() {
++        return this.adventureAuthor;
++    }
++
++    public net.kyori.adventure.key.@org.jspecify.annotations.NonNull Key assetId() {
++        return this.adventureAssetId;
++    }
++    // Paper end - name and author components, assetId key
++
+     @Override
+     public int getId() {
+         return CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT).getId(this.paintingVariant);