From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
Date: Sun, 28 Apr 2024 19:53:01 -0400
Subject: [PATCH] DataComponent API

Exposes the data component logic used by vanilla ItemStack to API
consumers as a version-specific API.
The types and methods introduced by this patch do not follow the general
API deprecation contracts and will be adapted to each new minecraft
release without backwards compatibility measures.

== AT ==
public net/minecraft/world/item/component/ItemContainerContents MAX_SIZE
public net/minecraft/world/item/component/ItemContainerContents items

diff --git a/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a49f65cae1354afbcd4afda07581790e06094be
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java
@@ -0,0 +1,36 @@
+package io.papermc.paper.datacomponent;
+
+import java.util.function.Function;
+import net.minecraft.core.component.DataComponentType;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.util.NullOps;
+import net.minecraft.util.Unit;
+import org.bukkit.craftbukkit.CraftRegistry;
+
+public record ComponentAdapter<NMS, API>(
+    DataComponentType<NMS> type,
+    Function<API, NMS> apiToVanilla,
+    Function<NMS, API> vanillaToApi,
+    boolean codecValidation
+) {
+    static final Function<Void, Unit> API_TO_UNIT_CONVERTER = $ -> Unit.INSTANCE;
+
+    public boolean isValued() {
+        return this.apiToVanilla != API_TO_UNIT_CONVERTER;
+    }
+
+    public NMS toVanilla(final API value) {
+        final NMS nms = this.apiToVanilla.apply(value);
+        if (this.codecValidation) {
+            this.type.codecOrThrow().encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NullOps.INSTANCE), nms).ifError(error -> {
+                throw new IllegalArgumentException("Failed to encode data component %s (%s)".formatted(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(this.type), error.message()));
+            });
+        }
+
+        return nms;
+    }
+
+    public API fromVanilla(final NMS value) {
+        return this.vanillaToApi.apply(value);
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee7adf16febfb508b14ff1453e73c75a42be7d26
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java
@@ -0,0 +1,171 @@
+package io.papermc.paper.datacomponent;
+
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.datacomponent.item.PaperBannerPatternLayers;
+import io.papermc.paper.datacomponent.item.PaperBlockItemDataProperties;
+import io.papermc.paper.datacomponent.item.PaperBundleContents;
+import io.papermc.paper.datacomponent.item.PaperChargedProjectiles;
+import io.papermc.paper.datacomponent.item.PaperConsumable;
+import io.papermc.paper.datacomponent.item.PaperCustomModelData;
+import io.papermc.paper.datacomponent.item.PaperDamageResistant;
+import io.papermc.paper.datacomponent.item.PaperDeathProtection;
+import io.papermc.paper.datacomponent.item.PaperDyedItemColor;
+import io.papermc.paper.datacomponent.item.PaperEnchantable;
+import io.papermc.paper.datacomponent.item.PaperEquippable;
+import io.papermc.paper.datacomponent.item.PaperFireworks;
+import io.papermc.paper.datacomponent.item.PaperFoodProperties;
+import io.papermc.paper.datacomponent.item.PaperItemAdventurePredicate;
+import io.papermc.paper.datacomponent.item.PaperItemArmorTrim;
+import io.papermc.paper.datacomponent.item.PaperItemAttributeModifiers;
+import io.papermc.paper.datacomponent.item.PaperItemContainerContents;
+import io.papermc.paper.datacomponent.item.PaperItemEnchantments;
+import io.papermc.paper.datacomponent.item.PaperItemLore;
+import io.papermc.paper.datacomponent.item.PaperItemTool;
+import io.papermc.paper.datacomponent.item.PaperJukeboxPlayable;
+import io.papermc.paper.datacomponent.item.PaperLodestoneTracker;
+import io.papermc.paper.datacomponent.item.PaperMapDecorations;
+import io.papermc.paper.datacomponent.item.PaperMapId;
+import io.papermc.paper.datacomponent.item.PaperMapItemColor;
+import io.papermc.paper.datacomponent.item.PaperOminousBottleAmplifier;
+import io.papermc.paper.datacomponent.item.PaperPotDecorations;
+import io.papermc.paper.datacomponent.item.PaperPotionContents;
+import io.papermc.paper.datacomponent.item.PaperRepairable;
+import io.papermc.paper.datacomponent.item.PaperResolvableProfile;
+import io.papermc.paper.datacomponent.item.PaperSeededContainerLoot;
+import io.papermc.paper.datacomponent.item.PaperSuspiciousStewEffects;
+import io.papermc.paper.datacomponent.item.PaperUnbreakable;
+import io.papermc.paper.datacomponent.item.PaperUseCooldown;
+import io.papermc.paper.datacomponent.item.PaperUseRemainder;
+import io.papermc.paper.datacomponent.item.PaperWritableBookContent;
+import io.papermc.paper.datacomponent.item.PaperWrittenBookContent;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import net.minecraft.core.component.DataComponentType;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.util.Unit;
+import net.minecraft.world.item.Rarity;
+import net.minecraft.world.item.component.MapPostProcessing;
+import org.bukkit.DyeColor;
+import org.bukkit.craftbukkit.CraftMusicInstrument;
+import org.bukkit.craftbukkit.inventory.CraftMetaFirework;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemRarity;
+
+import static io.papermc.paper.util.MCUtil.transformUnmodifiable;
+
+public final class ComponentAdapters {
+
+    static final Function<Unit, Void> UNIT_TO_API_CONVERTER = $ -> {
+        throw new UnsupportedOperationException("Cannot convert the Unit type to an API value");
+    };
+
+    static final Map<ResourceKey<DataComponentType<?>>, ComponentAdapter<?, ?>> ADAPTERS = new HashMap<>();
+
+    public static void bootstrap() {
+        registerIdentity(DataComponents.MAX_STACK_SIZE);
+        registerIdentity(DataComponents.MAX_DAMAGE);
+        registerIdentity(DataComponents.DAMAGE);
+        register(DataComponents.UNBREAKABLE, PaperUnbreakable::new);
+        register(DataComponents.CUSTOM_NAME, PaperAdventure::asAdventure, PaperAdventure::asVanilla);
+        register(DataComponents.ITEM_NAME, PaperAdventure::asAdventure, PaperAdventure::asVanilla);
+        register(DataComponents.ITEM_MODEL, CraftNamespacedKey::fromMinecraft, CraftNamespacedKey::toMinecraft);
+        register(DataComponents.LORE, PaperItemLore::new);
+        register(DataComponents.RARITY, nms -> ItemRarity.valueOf(nms.name()), api -> Rarity.valueOf(api.name()));
+        register(DataComponents.ENCHANTMENTS, PaperItemEnchantments::new);
+        register(DataComponents.CAN_PLACE_ON, PaperItemAdventurePredicate::new);
+        register(DataComponents.CAN_BREAK, PaperItemAdventurePredicate::new);
+        register(DataComponents.ATTRIBUTE_MODIFIERS, PaperItemAttributeModifiers::new);
+        register(DataComponents.CUSTOM_MODEL_DATA, PaperCustomModelData::new);
+        registerUntyped(DataComponents.HIDE_ADDITIONAL_TOOLTIP);
+        registerUntyped(DataComponents.HIDE_TOOLTIP);
+        registerIdentity(DataComponents.REPAIR_COST);
+        // registerUntyped(DataComponents.CREATIVE_SLOT_LOCK);
+        registerIdentity(DataComponents.ENCHANTMENT_GLINT_OVERRIDE);
+        registerUntyped(DataComponents.INTANGIBLE_PROJECTILE);
+        register(DataComponents.FOOD, PaperFoodProperties::new);
+        register(DataComponents.CONSUMABLE, PaperConsumable::new);
+        register(DataComponents.USE_REMAINDER, PaperUseRemainder::new);
+        register(DataComponents.USE_COOLDOWN, PaperUseCooldown::new);
+        register(DataComponents.DAMAGE_RESISTANT, PaperDamageResistant::new);
+        register(DataComponents.TOOL, PaperItemTool::new);
+        register(DataComponents.ENCHANTABLE, PaperEnchantable::new);
+        register(DataComponents.EQUIPPABLE, PaperEquippable::new);
+        register(DataComponents.REPAIRABLE, PaperRepairable::new);
+        registerUntyped(DataComponents.GLIDER);
+        register(DataComponents.TOOLTIP_STYLE, PaperAdventure::asAdventure, PaperAdventure::asVanilla);
+        register(DataComponents.DEATH_PROTECTION, PaperDeathProtection::new);
+        register(DataComponents.STORED_ENCHANTMENTS, PaperItemEnchantments::new);
+        register(DataComponents.DYED_COLOR, PaperDyedItemColor::new);
+        register(DataComponents.MAP_COLOR, PaperMapItemColor::new);
+        register(DataComponents.MAP_ID, PaperMapId::new);
+        register(DataComponents.MAP_DECORATIONS, PaperMapDecorations::new);
+        register(DataComponents.MAP_POST_PROCESSING, nms -> io.papermc.paper.item.MapPostProcessing.valueOf(nms.name()), api -> MapPostProcessing.valueOf(api.name()));
+        register(DataComponents.CHARGED_PROJECTILES, PaperChargedProjectiles::new);
+        register(DataComponents.BUNDLE_CONTENTS, PaperBundleContents::new);
+        register(DataComponents.POTION_CONTENTS, PaperPotionContents::new);
+        register(DataComponents.SUSPICIOUS_STEW_EFFECTS, PaperSuspiciousStewEffects::new);
+        register(DataComponents.WRITTEN_BOOK_CONTENT, PaperWrittenBookContent::new);
+        register(DataComponents.WRITABLE_BOOK_CONTENT, PaperWritableBookContent::new);
+        register(DataComponents.TRIM, PaperItemArmorTrim::new);
+        // debug stick state
+        // entity data
+        // bucket entity data
+        // block entity data
+        register(DataComponents.INSTRUMENT, CraftMusicInstrument::minecraftHolderToBukkit, CraftMusicInstrument::bukkitToMinecraftHolder);
+        register(DataComponents.OMINOUS_BOTTLE_AMPLIFIER, PaperOminousBottleAmplifier::new);
+        register(DataComponents.JUKEBOX_PLAYABLE, PaperJukeboxPlayable::new);
+        register(DataComponents.RECIPES,
+            nms -> transformUnmodifiable(nms, PaperAdventure::asAdventureKey),
+            api -> transformUnmodifiable(api, key -> PaperAdventure.asVanilla(Registries.RECIPE, key))
+        );
+        register(DataComponents.LODESTONE_TRACKER, PaperLodestoneTracker::new);
+        register(DataComponents.FIREWORK_EXPLOSION, CraftMetaFirework::getEffect, CraftMetaFirework::getExplosion);
+        register(DataComponents.FIREWORKS, PaperFireworks::new);
+        register(DataComponents.PROFILE, PaperResolvableProfile::new);
+        register(DataComponents.NOTE_BLOCK_SOUND, PaperAdventure::asAdventure, PaperAdventure::asVanilla);
+        register(DataComponents.BANNER_PATTERNS, PaperBannerPatternLayers::new);
+        register(DataComponents.BASE_COLOR, nms -> DyeColor.getByWoolData((byte) nms.getId()), api -> net.minecraft.world.item.DyeColor.byId(api.getWoolData()));
+        register(DataComponents.POT_DECORATIONS, PaperPotDecorations::new);
+        register(DataComponents.CONTAINER, PaperItemContainerContents::new);
+        register(DataComponents.BLOCK_STATE, PaperBlockItemDataProperties::new);
+        // bees
+        // register(DataComponents.LOCK, PaperLockCode::new);
+        register(DataComponents.CONTAINER_LOOT, PaperSeededContainerLoot::new);
+
+        // TODO: REMOVE THIS... we want to build the PR... so lets just make things UNTYPED!
+        for (final Map.Entry<ResourceKey<DataComponentType<?>>, DataComponentType<?>> componentType : BuiltInRegistries.DATA_COMPONENT_TYPE.entrySet()) {
+            if (!ADAPTERS.containsKey(componentType.getKey())) {
+                registerUntyped((DataComponentType<Unit>) componentType.getValue());
+            }
+        }
+    }
+
+    public static void registerUntyped(final DataComponentType<Unit> type) {
+        registerInternal(type, UNIT_TO_API_CONVERTER, ComponentAdapter.API_TO_UNIT_CONVERTER, false);
+    }
+
+    private static <COMMON> void registerIdentity(final DataComponentType<COMMON> type) {
+        registerInternal(type, Function.identity(), Function.identity(), true);
+    }
+
+    private static <NMS, API extends Handleable<NMS>> void register(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi) {
+        registerInternal(type, vanillaToApi, Handleable::getHandle, false);
+    }
+
+    private static <NMS, API> void register(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi, final Function<API, NMS> apiToVanilla) {
+        registerInternal(type, vanillaToApi, apiToVanilla, false);
+    }
+
+    private static <NMS, API> void registerInternal(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi, final Function<API, NMS> apiToVanilla, final boolean codecValidation) {
+        final ResourceKey<DataComponentType<?>> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow();
+        if (ADAPTERS.containsKey(key)) {
+            throw new IllegalStateException("Duplicate adapter registration for " + key);
+        }
+        ADAPTERS.put(key, new ComponentAdapter<>(type, apiToVanilla, vanillaToApi, codecValidation && !type.isTransient()));
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java b/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0d4ec462eee47840e91bac888ae46045b493f07
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java
@@ -0,0 +1,109 @@
+package io.papermc.paper.datacomponent;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import net.minecraft.core.component.DataComponentMap;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Registry;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jspecify.annotations.Nullable;
+
+public abstract class PaperComponentType<T, NMS> implements DataComponentType, Handleable<net.minecraft.core.component.DataComponentType<NMS>> {
+
+    static {
+        ComponentAdapters.bootstrap();
+    }
+
+    public static <T> net.minecraft.core.component.DataComponentType<T> bukkitToMinecraft(final DataComponentType type) {
+        return CraftRegistry.bukkitToMinecraft(type);
+    }
+
+    public static DataComponentType minecraftToBukkit(final net.minecraft.core.component.DataComponentType<?> type) {
+        return CraftRegistry.minecraftToBukkit(type, Registries.DATA_COMPONENT_TYPE, Registry.DATA_COMPONENT_TYPE);
+    }
+
+    public static Set<DataComponentType> minecraftToBukkit(final Set<net.minecraft.core.component.DataComponentType<?>> nmsTypes) {
+        final Set<DataComponentType> types = new HashSet<>(nmsTypes.size());
+        for (final net.minecraft.core.component.DataComponentType<?> nmsType : nmsTypes) {
+            types.add(PaperComponentType.minecraftToBukkit(nmsType));
+        }
+        return Collections.unmodifiableSet(types);
+    }
+
+    public static <B, M> @Nullable B convertDataComponentValue(final DataComponentMap map, final PaperComponentType.ValuedImpl<B, M> type) {
+        final net.minecraft.core.component.DataComponentType<M> nms = bukkitToMinecraft(type);
+        final M nmsValue = map.get(nms);
+        if (nmsValue == null) {
+            return null;
+        }
+        return type.getAdapter().fromVanilla(nmsValue);
+    }
+
+    private final NamespacedKey key;
+    private final net.minecraft.core.component.DataComponentType<NMS> type;
+    private final ComponentAdapter<NMS, T> adapter;
+
+    public PaperComponentType(final NamespacedKey key, final net.minecraft.core.component.DataComponentType<NMS> type, final ComponentAdapter<NMS, T> adapter) {
+        this.key = key;
+        this.type = type;
+        this.adapter = adapter;
+    }
+
+    @Override
+    public NamespacedKey getKey() {
+        return this.key;
+    }
+
+    @Override
+    public boolean isPersistent() {
+        return !this.type.isTransient();
+    }
+
+    public ComponentAdapter<NMS, T> getAdapter() {
+        return this.adapter;
+    }
+
+    @Override
+    public net.minecraft.core.component.DataComponentType<NMS> getHandle() {
+        return this.type;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <NMS> DataComponentType of(final NamespacedKey key, final net.minecraft.core.component.DataComponentType<NMS> type) {
+        final ComponentAdapter<NMS, ?> adapter = (ComponentAdapter<NMS, ?>) ComponentAdapters.ADAPTERS.get(BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow());
+        if (adapter == null) {
+            throw new IllegalArgumentException("No adapter found for " + key);
+        }
+        if (adapter.isValued()) {
+            return new ValuedImpl<>(key, type, adapter);
+        } else {
+            return new NonValuedImpl<>(key, type, adapter);
+        }
+    }
+
+    public static final class NonValuedImpl<T, NMS> extends PaperComponentType<T, NMS> implements NonValued {
+
+        NonValuedImpl(
+            final NamespacedKey key,
+            final net.minecraft.core.component.DataComponentType<NMS> type,
+            final ComponentAdapter<NMS, T> adapter
+        ) {
+            super(key, type, adapter);
+        }
+    }
+
+    public static final class ValuedImpl<T, NMS> extends PaperComponentType<T, NMS> implements Valued<T> {
+
+        ValuedImpl(
+            final NamespacedKey key,
+            final net.minecraft.core.component.DataComponentType<NMS> type,
+            final ComponentAdapter<NMS, T> adapter
+        ) {
+            super(key, type, adapter);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..9111c3095986bea43d5eb06763cbe287f6853434
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java
@@ -0,0 +1,239 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.google.common.base.Preconditions;
+import io.papermc.paper.registry.PaperRegistries;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import io.papermc.paper.registry.tag.TagKey;
+import io.papermc.paper.text.Filtered;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.util.TriState;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.world.item.component.OminousBottleAmplifier;
+import org.bukkit.JukeboxSong;
+import org.bukkit.block.BlockType;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.damage.DamageType;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ItemType;
+import org.bukkit.inventory.meta.trim.ArmorTrim;
+import org.bukkit.map.MapCursor;
+import org.jspecify.annotations.Nullable;
+
+public final class ItemComponentTypesBridgesImpl implements ItemComponentTypesBridge {
+
+    @Override
+    public ChargedProjectiles.Builder chargedProjectiles() {
+        return new PaperChargedProjectiles.BuilderImpl();
+    }
+
+    @Override
+    public PotDecorations.Builder potDecorations() {
+        return new PaperPotDecorations.BuilderImpl();
+    }
+
+    @Override
+    public Unbreakable.Builder unbreakable() {
+        return new PaperUnbreakable.BuilderImpl();
+    }
+
+    @Override
+    public ItemLore.Builder lore() {
+        return new PaperItemLore.BuilderImpl();
+    }
+
+    @Override
+    public ItemEnchantments.Builder enchantments() {
+        return new PaperItemEnchantments.BuilderImpl();
+    }
+
+    @Override
+    public ItemAttributeModifiers.Builder modifiers() {
+        return new PaperItemAttributeModifiers.BuilderImpl();
+    }
+
+    @Override
+    public FoodProperties.Builder food() {
+        return new PaperFoodProperties.BuilderImpl();
+    }
+
+    @Override
+    public DyedItemColor.Builder dyedItemColor() {
+        return new PaperDyedItemColor.BuilderImpl();
+    }
+
+    @Override
+    public PotionContents.Builder potionContents() {
+        return new PaperPotionContents.BuilderImpl();
+    }
+
+    @Override
+    public BundleContents.Builder bundleContents() {
+        return new PaperBundleContents.BuilderImpl();
+    }
+
+    @Override
+    public SuspiciousStewEffects.Builder suspiciousStewEffects() {
+        return new PaperSuspiciousStewEffects.BuilderImpl();
+    }
+
+    @Override
+    public MapItemColor.Builder mapItemColor() {
+        return new PaperMapItemColor.BuilderImpl();
+    }
+
+    @Override
+    public MapDecorations.Builder mapDecorations() {
+        return new PaperMapDecorations.BuilderImpl();
+    }
+
+    @Override
+    public MapDecorations.DecorationEntry decorationEntry(final MapCursor.Type type, final double x, final double z, final float rotation) {
+        return PaperMapDecorations.PaperDecorationEntry.toApi(type, x, z, rotation);
+    }
+
+    @Override
+    public SeededContainerLoot.Builder seededContainerLoot(final Key lootTableKey) {
+        return new PaperSeededContainerLoot.BuilderImpl(lootTableKey);
+    }
+
+    @Override
+    public ItemContainerContents.Builder itemContainerContents() {
+        return new PaperItemContainerContents.BuilderImpl();
+    }
+
+    @Override
+    public JukeboxPlayable.Builder jukeboxPlayable(final JukeboxSong song) {
+        return new PaperJukeboxPlayable.BuilderImpl(song);
+    }
+
+    @Override
+    public Tool.Builder tool() {
+        return new PaperItemTool.BuilderImpl();
+    }
+
+    @Override
+    public Tool.Rule rule(final RegistryKeySet<BlockType> blocks, final @Nullable Float speed, final TriState correctForDrops) {
+        return PaperItemTool.PaperRule.fromUnsafe(blocks, speed, correctForDrops);
+    }
+
+    @Override
+    public ItemAdventurePredicate.Builder itemAdventurePredicate() {
+        return new PaperItemAdventurePredicate.BuilderImpl();
+    }
+
+    @Override
+    public WrittenBookContent.Builder writtenBookContent(final Filtered<String> title, final String author) {
+        return new PaperWrittenBookContent.BuilderImpl(title, author);
+    }
+
+    @Override
+    public WritableBookContent.Builder writeableBookContent() {
+        return new PaperWritableBookContent.BuilderImpl();
+    }
+
+    @Override
+    public ItemArmorTrim.Builder itemArmorTrim(final ArmorTrim armorTrim) {
+        return new PaperItemArmorTrim.BuilderImpl(armorTrim);
+    }
+
+    @Override
+    public LodestoneTracker.Builder lodestoneTracker() {
+        return new PaperLodestoneTracker.BuilderImpl();
+    }
+
+    @Override
+    public Fireworks.Builder fireworks() {
+        return new PaperFireworks.BuilderImpl();
+    }
+
+    @Override
+    public ResolvableProfile.Builder resolvableProfile() {
+        return new PaperResolvableProfile.BuilderImpl();
+    }
+
+    @Override
+    public ResolvableProfile resolvableProfile(final PlayerProfile profile) {
+        return PaperResolvableProfile.toApi(profile);
+    }
+
+    @Override
+    public BannerPatternLayers.Builder bannerPatternLayers() {
+        return new PaperBannerPatternLayers.BuilderImpl();
+    }
+
+    @Override
+    public BlockItemDataProperties.Builder blockItemStateProperties() {
+        return new PaperBlockItemDataProperties.BuilderImpl();
+    }
+
+    @Override
+    public MapId mapId(final int id) {
+        return new PaperMapId(new net.minecraft.world.level.saveddata.maps.MapId(id));
+    }
+
+    @Override
+    public UseRemainder useRemainder(final ItemStack itemStack) {
+        Preconditions.checkArgument(itemStack != null, "Item cannot be null");
+        Preconditions.checkArgument(!itemStack.isEmpty(), "Remaining item cannot be empty!");
+        return new PaperUseRemainder(
+            new net.minecraft.world.item.component.UseRemainder(CraftItemStack.asNMSCopy(itemStack))
+        );
+    }
+
+    @Override
+    public Consumable.Builder consumable() {
+        return new PaperConsumable.BuilderImpl();
+    }
+
+    @Override
+    public UseCooldown.Builder useCooldown(final float seconds) {
+        Preconditions.checkArgument(seconds > 0, "seconds must be positive, was %s", seconds);
+        return new PaperUseCooldown.BuilderImpl(seconds);
+    }
+
+    @Override
+    public DamageResistant damageResistant(final TagKey<DamageType> types) {
+        return new PaperDamageResistant(new net.minecraft.world.item.component.DamageResistant(PaperRegistries.toNms(types)));
+    }
+
+    @Override
+    public Enchantable enchantable(final int level) {
+        return new PaperEnchantable(new net.minecraft.world.item.enchantment.Enchantable(level));
+    }
+
+    @Override
+    public Repairable repairable(final RegistryKeySet<ItemType> types) {
+        return new PaperRepairable(new net.minecraft.world.item.enchantment.Repairable(
+            PaperRegistrySets.convertToNms(Registries.ITEM, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), types)
+        ));
+    }
+
+    @Override
+    public Equippable.Builder equippable(EquipmentSlot slot) {
+        return new PaperEquippable.BuilderImpl(slot);
+    }
+
+    @Override
+    public DeathProtection.Builder deathProtection() {
+        return new PaperDeathProtection.BuilderImpl();
+    }
+
+    @Override
+    public CustomModelData customModelData(final int id) {
+        return new PaperCustomModelData(new net.minecraft.world.item.component.CustomModelData(id));
+    }
+
+    @Override
+    public PaperOminousBottleAmplifier ominousBottleAmplifier(final int amplifier) {
+        Preconditions.checkArgument(OminousBottleAmplifier.MIN_AMPLIFIER <= amplifier && amplifier <= OminousBottleAmplifier.MAX_AMPLIFIER,
+            "amplifier must be between %s-%s, was %s", OminousBottleAmplifier.MIN_AMPLIFIER, OminousBottleAmplifier.MAX_AMPLIFIER, amplifier
+        );
+        return new PaperOminousBottleAmplifier(
+            new OminousBottleAmplifier(amplifier)
+        );
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca49c2d2e1edcf6c4f7a5ca6c9ba96920aa385f4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java
@@ -0,0 +1,62 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.registry.RegistryAccess;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.util.MCUtil;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import org.bukkit.DyeColor;
+import org.bukkit.block.banner.Pattern;
+import org.bukkit.block.banner.PatternType;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.bukkit.craftbukkit.block.banner.CraftPatternType;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperBannerPatternLayers(
+    net.minecraft.world.level.block.entity.BannerPatternLayers impl
+) implements BannerPatternLayers, Handleable<net.minecraft.world.level.block.entity.BannerPatternLayers> {
+
+    private static List<Pattern> convert(final net.minecraft.world.level.block.entity.BannerPatternLayers nmsPatterns) {
+        return MCUtil.transformUnmodifiable(nmsPatterns.layers(), input -> {
+            final Optional<PatternType> type = CraftRegistry.unwrapAndConvertHolder(RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN), input.pattern());
+            return new Pattern(Objects.requireNonNull(DyeColor.getByWoolData((byte) input.color().getId())), type.orElseThrow(() -> new IllegalStateException("Inlined banner patterns are not supported yet in the API!")));
+        });
+    }
+
+    @Override
+    public net.minecraft.world.level.block.entity.BannerPatternLayers getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<Pattern> patterns() {
+        return convert(impl);
+    }
+
+    static final class BuilderImpl implements BannerPatternLayers.Builder {
+
+        private final net.minecraft.world.level.block.entity.BannerPatternLayers.Builder builder = new net.minecraft.world.level.block.entity.BannerPatternLayers.Builder();
+
+        @Override
+        public BannerPatternLayers.Builder add(final Pattern pattern) {
+            this.builder.add(
+                CraftPatternType.bukkitToMinecraftHolder(pattern.getPattern()),
+                net.minecraft.world.item.DyeColor.byId(pattern.getColor().getWoolData())
+            );
+            return this;
+        }
+
+        @Override
+        public BannerPatternLayers.Builder addAll(final List<Pattern> patterns) {
+            patterns.forEach(this::add);
+            return this;
+        }
+
+        @Override
+        public BannerPatternLayers build() {
+            return new PaperBannerPatternLayers(this.builder.build());
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..5757e16c5948a6897bc61005ea7260940a49abfe
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.datacomponent.item;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import java.util.Map;
+import net.minecraft.world.item.component.BlockItemStateProperties;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.block.BlockType;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.craftbukkit.block.CraftBlockType;
+import org.bukkit.craftbukkit.block.data.CraftBlockData;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperBlockItemDataProperties(
+    BlockItemStateProperties impl
+) implements BlockItemDataProperties, Handleable<BlockItemStateProperties> {
+
+    @Override
+    public BlockData createBlockData(final BlockType blockType) {
+        final Block block = CraftBlockType.bukkitToMinecraftNew(blockType);
+        final BlockState defaultState = block.defaultBlockState();
+        return this.impl.apply(defaultState).createCraftBlockData();
+    }
+
+    @Override
+    public BlockData applyTo(final BlockData blockData) {
+        final BlockState state = ((CraftBlockData) blockData).getState();
+        return this.impl.apply(state).createCraftBlockData();
+    }
+
+    @Override
+    public BlockItemStateProperties getHandle() {
+        return this.impl;
+    }
+
+    static final class BuilderImpl implements BlockItemDataProperties.Builder {
+
+        private final Map<String, String> properties = new Object2ObjectOpenHashMap<>();
+
+        // TODO when BlockProperty API is merged
+
+        @Override
+        public BlockItemDataProperties build() {
+            if (this.properties.isEmpty()) {
+                return new PaperBlockItemDataProperties(BlockItemStateProperties.EMPTY);
+            }
+            return new PaperBlockItemDataProperties(new BlockItemStateProperties(new Object2ObjectOpenHashMap<>(this.properties)));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba95ce77dbddb90fd2616c9112fd74051dddc3ee
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java
@@ -0,0 +1,51 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemStack;
+
+public record PaperBundleContents(
+    net.minecraft.world.item.component.BundleContents impl
+) implements BundleContents, Handleable<net.minecraft.world.item.component.BundleContents> {
+
+    @Override
+    public net.minecraft.world.item.component.BundleContents getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public List<ItemStack> contents() {
+        return MCUtil.transformUnmodifiable((List<net.minecraft.world.item.ItemStack>) this.impl.items(), CraftItemStack::asBukkitCopy);
+    }
+
+    static final class BuilderImpl implements BundleContents.Builder {
+
+        private final List<net.minecraft.world.item.ItemStack> items = new ObjectArrayList<>();
+
+        @Override
+        public BundleContents.Builder add(final ItemStack stack) {
+            Preconditions.checkArgument(stack != null, "stack cannot be null");
+            Preconditions.checkArgument(!stack.isEmpty(), "stack cannot be empty");
+            this.items.add(CraftItemStack.asNMSCopy(stack));
+            return this;
+        }
+
+        @Override
+        public BundleContents.Builder addAll(final List<ItemStack> stacks) {
+            stacks.forEach(this::add);
+            return this;
+        }
+
+        @Override
+        public BundleContents build() {
+            if (this.items.isEmpty()) {
+                return new PaperBundleContents(net.minecraft.world.item.component.BundleContents.EMPTY);
+            }
+            return new PaperBundleContents(new net.minecraft.world.item.component.BundleContents(new ObjectArrayList<>(this.items)));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java
new file mode 100644
index 0000000000000000000000000000000000000000..2129dd67fd02a13f6e6fbdfb07505dc64307a3f0
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java
@@ -0,0 +1,51 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import java.util.ArrayList;
+import java.util.List;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemStack;
+
+public record PaperChargedProjectiles(
+    net.minecraft.world.item.component.ChargedProjectiles impl
+) implements ChargedProjectiles, Handleable<net.minecraft.world.item.component.ChargedProjectiles> {
+
+    @Override
+    public net.minecraft.world.item.component.ChargedProjectiles getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public List<ItemStack> projectiles() {
+        return MCUtil.transformUnmodifiable(this.impl.getItems() /*makes copies internally*/, CraftItemStack::asCraftMirror);
+    }
+
+    static final class BuilderImpl implements ChargedProjectiles.Builder {
+
+        private final List<net.minecraft.world.item.ItemStack> items = new ArrayList<>();
+
+        @Override
+        public ChargedProjectiles.Builder add(final ItemStack stack) {
+            Preconditions.checkArgument(stack != null, "stack cannot be null");
+            Preconditions.checkArgument(!stack.isEmpty(), "stack cannot be empty");
+            this.items.add(CraftItemStack.asNMSCopy(stack));
+            return this;
+        }
+
+        @Override
+        public ChargedProjectiles.Builder addAll(final List<ItemStack> stacks) {
+            stacks.forEach(this::add);
+            return this;
+        }
+
+        @Override
+        public ChargedProjectiles build() {
+            if (this.items.isEmpty()) {
+                return new PaperChargedProjectiles(net.minecraft.world.item.component.ChargedProjectiles.EMPTY);
+            }
+            return new PaperChargedProjectiles(net.minecraft.world.item.component.ChargedProjectiles.of(this.items));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bc2bad71d6945ca24f37008effc903a84466004
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java
@@ -0,0 +1,126 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect;
+import io.papermc.paper.datacomponent.item.consumable.ItemUseAnimation;
+import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import net.kyori.adventure.key.Key;
+import net.minecraft.core.Holder;
+import net.minecraft.sounds.SoundEvent;
+import net.minecraft.sounds.SoundEvents;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.checkerframework.checker.index.qual.NonNegative;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperConsumable(
+    net.minecraft.world.item.component.Consumable impl
+) implements Consumable, Handleable<net.minecraft.world.item.component.Consumable> {
+
+    private static final ItemUseAnimation[] VALUES = ItemUseAnimation.values();
+
+    @Override
+    public net.minecraft.world.item.component.Consumable getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @NonNegative float consumeSeconds() {
+        return this.impl.consumeSeconds();
+    }
+
+    @Override
+    public ItemUseAnimation animation() {
+        return VALUES[this.impl.animation().ordinal()];
+    }
+
+    @Override
+    public Key sound() {
+        return PaperAdventure.asAdventure(this.impl.sound().value().location());
+    }
+
+    @Override
+    public boolean hasConsumeParticles() {
+        return this.impl.hasConsumeParticles();
+    }
+
+    @Override
+    public @Unmodifiable List<ConsumeEffect> consumeEffects() {
+        return MCUtil.transformUnmodifiable(this.impl.onConsumeEffects(), PaperConsumableEffects::fromNms);
+    }
+
+    @Override
+    public Consumable.Builder toBuilder() {
+        return new BuilderImpl()
+            .consumeSeconds(this.consumeSeconds())
+            .animation(this.animation())
+            .sound(this.sound())
+            .addEffects(this.consumeEffects());
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private static final net.minecraft.world.item.ItemUseAnimation[] VALUES = net.minecraft.world.item.ItemUseAnimation.values();
+
+        private float consumeSeconds = net.minecraft.world.item.component.Consumable.DEFAULT_CONSUME_SECONDS;
+        private net.minecraft.world.item.ItemUseAnimation consumeAnimation = net.minecraft.world.item.ItemUseAnimation.EAT;
+        private Holder<SoundEvent> eatSound = SoundEvents.GENERIC_EAT;
+        private boolean hasConsumeParticles = true;
+        private final List<net.minecraft.world.item.consume_effects.ConsumeEffect> effects = new ObjectArrayList<>();
+
+        @Override
+        public Builder consumeSeconds(final @NonNegative float consumeSeconds) {
+            Preconditions.checkArgument(consumeSeconds >= 0, "consumeSeconds must be non-negative, was %s", consumeSeconds);
+            this.consumeSeconds = consumeSeconds;
+            return this;
+        }
+
+        @Override
+        public Builder animation(final ItemUseAnimation animation) {
+            this.consumeAnimation = VALUES[animation.ordinal()];
+            return this;
+        }
+
+        @Override
+        public Builder sound(final Key sound) {
+            this.eatSound = PaperAdventure.resolveSound(sound);
+            return this;
+        }
+
+        @Override
+        public Builder hasConsumeParticles(final boolean hasConsumeParticles) {
+            this.hasConsumeParticles = hasConsumeParticles;
+            return this;
+        }
+
+        @Override
+        public Builder addEffect(final ConsumeEffect effect) {
+            this.effects.add(PaperConsumableEffects.toNms(effect));
+            return this;
+        }
+
+        @Override
+        public Builder addEffects(final List<ConsumeEffect> effects) {
+            for (final ConsumeEffect effect : effects) {
+                this.effects.add(PaperConsumableEffects.toNms(effect));
+            }
+            return this;
+        }
+
+        @Override
+        public Consumable build() {
+            return new PaperConsumable(
+                new net.minecraft.world.item.component.Consumable(
+                    this.consumeSeconds,
+                    this.consumeAnimation,
+                    this.eatSound,
+                    this.hasConsumeParticles,
+                    new ObjectArrayList<>(this.effects)
+                )
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java
new file mode 100644
index 0000000000000000000000000000000000000000..8373d882e8b927e74961d5ed2d548b2db6dacdaf
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperCustomModelData(
+    net.minecraft.world.item.component.CustomModelData impl
+) implements CustomModelData, Handleable<net.minecraft.world.item.component.CustomModelData> {
+
+    @Override
+    public net.minecraft.world.item.component.CustomModelData getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public int id() {
+        return this.impl.value();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java
new file mode 100644
index 0000000000000000000000000000000000000000..adc986c8b3d65e3fb91a8951048194bbe4052b74
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.registry.PaperRegistries;
+import io.papermc.paper.registry.tag.TagKey;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.damage.DamageType;
+
+public record PaperDamageResistant(
+    net.minecraft.world.item.component.DamageResistant impl
+) implements DamageResistant, Handleable<net.minecraft.world.item.component.DamageResistant> {
+
+    @Override
+    public net.minecraft.world.item.component.DamageResistant getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public TagKey<DamageType> types() {
+        return PaperRegistries.fromNms(this.impl.types());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java
new file mode 100644
index 0000000000000000000000000000000000000000..798e45d3b3e895f8b3abb9db1c9d58348bcd22d3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect;
+import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects;
+import io.papermc.paper.util.MCUtil;
+import java.util.ArrayList;
+import java.util.List;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperDeathProtection(
+    net.minecraft.world.item.component.DeathProtection impl
+) implements DeathProtection, Handleable<net.minecraft.world.item.component.DeathProtection> {
+
+    @Override
+    public net.minecraft.world.item.component.DeathProtection getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<ConsumeEffect> deathEffects() {
+        return MCUtil.transformUnmodifiable(this.impl.deathEffects(), PaperConsumableEffects::fromNms);
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private final List<net.minecraft.world.item.consume_effects.ConsumeEffect> effects = new ArrayList<>();
+
+        @Override
+        public Builder addEffect(final ConsumeEffect effect) {
+            this.effects.add(PaperConsumableEffects.toNms(effect));
+            return this;
+        }
+
+        @Override
+        public Builder addEffects(final List<ConsumeEffect> effects) {
+            for (final ConsumeEffect effect : effects) {
+                this.effects.add(PaperConsumableEffects.toNms(effect));
+            }
+            return this;
+        }
+
+        @Override
+        public DeathProtection build() {
+            return new PaperDeathProtection(
+                new net.minecraft.world.item.component.DeathProtection(this.effects)
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java
new file mode 100644
index 0000000000000000000000000000000000000000..2407d79e2e77e8be6de8e65769efc4d79e3be9db
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java
@@ -0,0 +1,52 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.Color;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperDyedItemColor(
+    net.minecraft.world.item.component.DyedItemColor impl
+) implements DyedItemColor, Handleable<net.minecraft.world.item.component.DyedItemColor> {
+
+    @Override
+    public net.minecraft.world.item.component.DyedItemColor getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public Color color() {
+        return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public DyedItemColor showInTooltip(final boolean showInTooltip) {
+        return new PaperDyedItemColor(this.impl.withTooltip(showInTooltip));
+    }
+
+    static final class BuilderImpl implements DyedItemColor.Builder {
+
+        private Color color = Color.WHITE;
+        private boolean showInToolTip = true;
+
+        @Override
+        public DyedItemColor.Builder color(final Color color) {
+            this.color = color;
+            return this;
+        }
+
+        @Override
+        public DyedItemColor.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInToolTip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public DyedItemColor build() {
+            return new PaperDyedItemColor(new net.minecraft.world.item.component.DyedItemColor(this.color.asRGB(), this.showInToolTip));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java
new file mode 100644
index 0000000000000000000000000000000000000000..422e1a4d606481f0dc68843fbbc8126ccfda1cc3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperEnchantable(
+    net.minecraft.world.item.enchantment.Enchantable impl
+) implements Enchantable, Handleable<net.minecraft.world.item.enchantment.Enchantable> {
+
+    @Override
+    public net.minecraft.world.item.enchantment.Enchantable getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public int value() {
+        return this.impl.value();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab7947bddfad12d8ae7abcda34f17c3335082a01
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java
@@ -0,0 +1,168 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import java.util.Optional;
+import net.kyori.adventure.key.Key;
+import net.minecraft.core.Holder;
+import net.minecraft.core.HolderSet;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.sounds.SoundEvent;
+import net.minecraft.sounds.SoundEvents;
+import org.bukkit.craftbukkit.CraftEquipmentSlot;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.entity.EntityType;
+import org.bukkit.inventory.EquipmentSlot;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public record PaperEquippable(
+    net.minecraft.world.item.equipment.Equippable impl
+) implements Equippable, Handleable<net.minecraft.world.item.equipment.Equippable> {
+
+    @Override
+    public net.minecraft.world.item.equipment.Equippable getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public EquipmentSlot slot() {
+        return CraftEquipmentSlot.getSlot(this.impl.slot());
+    }
+
+    @Override
+    public Key equipSound() {
+        return PaperAdventure.asAdventure(this.impl.equipSound().value().location());
+    }
+
+    @Override
+    public @Nullable Key model() {
+        return this.impl.model()
+            .map(PaperAdventure::asAdventure)
+            .orElse(null);
+    }
+
+    @Override
+    public @Nullable Key cameraOverlay() {
+        return this.impl.cameraOverlay()
+            .map(PaperAdventure::asAdventure)
+            .orElse(null);
+    }
+
+    @Override
+    public @Nullable RegistryKeySet<EntityType> allowedEntities() {
+        return this.impl.allowedEntities()
+            .map((set) -> PaperRegistrySets.convertToApi(RegistryKey.ENTITY_TYPE, set))
+            .orElse(null);
+    }
+
+    @Override
+    public boolean dispensable() {
+        return this.impl.dispensable();
+    }
+
+    @Override
+    public boolean swappable() {
+        return this.impl.swappable();
+    }
+
+    @Override
+    public boolean damageOnHurt() {
+        return this.impl.damageOnHurt();
+    }
+
+    @Override
+    public Builder toBuilder() {
+        return new BuilderImpl(this.slot())
+            .equipSound(this.equipSound())
+            .model(this.model())
+            .cameraOverlay(this.cameraOverlay())
+            .allowedEntities(this.allowedEntities())
+            .dispensable(this.dispensable())
+            .swappable(this.swappable())
+            .damageOnHurt(this.damageOnHurt());
+    }
+
+
+    static final class BuilderImpl implements Builder {
+
+        private final net.minecraft.world.entity.EquipmentSlot equipmentSlot;
+        private Holder<SoundEvent> equipSound = SoundEvents.ARMOR_EQUIP_GENERIC;
+        private Optional<ResourceLocation> model = Optional.empty();
+        private Optional<ResourceLocation> cameraOverlay = Optional.empty();
+        private Optional<HolderSet<net.minecraft.world.entity.EntityType<?>>> allowedEntities = Optional.empty();
+        private boolean dispensable = true;
+        private boolean swappable = true;
+        private boolean damageOnHurt = true;
+
+        BuilderImpl(final EquipmentSlot equipmentSlot) {
+            this.equipmentSlot = CraftEquipmentSlot.getNMS(equipmentSlot);
+        }
+
+        @Override
+        public Builder equipSound(final Key sound) {
+            this.equipSound = PaperAdventure.resolveSound(sound);
+            return this;
+        }
+
+        @Override
+        public Builder model(final @Nullable Key model) {
+            this.model = Optional.ofNullable(model)
+                .map(PaperAdventure::asVanilla);
+
+            return this;
+        }
+
+        @Override
+        public Builder cameraOverlay(@Nullable final Key cameraOverlay) {
+            this.cameraOverlay = Optional.ofNullable(cameraOverlay)
+                .map(PaperAdventure::asVanilla);
+
+            return this;
+        }
+
+        @Override
+        public Builder allowedEntities(final @Nullable RegistryKeySet<EntityType> allowedEntities) {
+            this.allowedEntities = Optional.ofNullable(allowedEntities)
+                .map((set) -> PaperRegistrySets.convertToNms(Registries.ENTITY_TYPE, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), set));
+            return this;
+        }
+
+        @Override
+        public Builder dispensable(final boolean dispensable) {
+            this.dispensable = dispensable;
+            return this;
+        }
+
+        @Override
+        public Builder swappable(final boolean swappable) {
+            this.swappable = swappable;
+            return this;
+        }
+
+        @Override
+        public Builder damageOnHurt(final boolean damageOnHurt) {
+            this.damageOnHurt = damageOnHurt;
+            return this;
+        }
+
+        @Override
+        public Equippable build() {
+            return new PaperEquippable(
+                new net.minecraft.world.item.equipment.Equippable(
+                    this.equipmentSlot,
+                    this.equipSound,
+                    this.model,
+                    this.cameraOverlay,
+                    this.allowedEntities,
+                    this.dispensable,
+                    this.swappable,
+                    this.damageOnHurt
+                )
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java
new file mode 100644
index 0000000000000000000000000000000000000000..80189eb5054a044a76f19200eb0e5f316c30de92
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java
@@ -0,0 +1,73 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import net.minecraft.world.item.component.FireworkExplosion;
+import org.bukkit.FireworkEffect;
+import org.bukkit.craftbukkit.inventory.CraftMetaFirework;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperFireworks(
+    net.minecraft.world.item.component.Fireworks impl
+) implements Fireworks, Handleable<net.minecraft.world.item.component.Fireworks> {
+
+    @Override
+    public net.minecraft.world.item.component.Fireworks getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<FireworkEffect> effects() {
+        return MCUtil.transformUnmodifiable(this.impl.explosions(), CraftMetaFirework::getEffect);
+    }
+
+    @Override
+    public int flightDuration() {
+        return this.impl.flightDuration();
+    }
+
+    static final class BuilderImpl implements Fireworks.Builder {
+
+        private final List<FireworkExplosion> effects = new ObjectArrayList<>();
+        private int duration = 0; // default set from nms Fireworks component
+
+        @Override
+        public Fireworks.Builder flightDuration(final int duration) {
+            Preconditions.checkArgument(duration >= 0 && duration <= 0xFF, "duration must be an unsigned byte ([%s, %s]), was %s", 0, 0xFF, duration);
+            this.duration = duration;
+            return this;
+        }
+
+        @Override
+        public Fireworks.Builder addEffect(final FireworkEffect effect) {
+            Preconditions.checkArgument(
+                this.effects.size() + 1 <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
+                "Cannot have more than %s effects, had %s",
+                net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
+                this.effects.size() + 1
+            );
+            this.effects.add(CraftMetaFirework.getExplosion(effect));
+            return this;
+        }
+
+        @Override
+        public Fireworks.Builder addEffects(final List<FireworkEffect> effects) {
+            Preconditions.checkArgument(
+                this.effects.size() + effects.size() <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
+                "Cannot have more than %s effects, had %s",
+                net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
+                this.effects.size() + effects.size()
+            );
+            MCUtil.addAndConvert(this.effects, effects, CraftMetaFirework::getExplosion);
+            return this;
+        }
+
+        @Override
+        public Fireworks build() {
+            return new PaperFireworks(new net.minecraft.world.item.component.Fireworks(this.duration, new ObjectArrayList<>(this.effects)));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a043bb9001048f66d3a6aa8cb896b35bd2df606
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java
@@ -0,0 +1,72 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperFoodProperties(
+    net.minecraft.world.food.FoodProperties impl
+) implements FoodProperties, Handleable<net.minecraft.world.food.FoodProperties> {
+
+    @Override
+    public int nutrition() {
+        return this.impl.nutrition();
+    }
+
+    @Override
+    public float saturation() {
+        return this.impl.saturation();
+    }
+
+    @Override
+    public boolean canAlwaysEat() {
+        return this.impl.canAlwaysEat();
+    }
+
+    @Override
+    public FoodProperties.Builder toBuilder() {
+        return new BuilderImpl()
+            .nutrition(this.nutrition())
+            .saturation(this.saturation())
+            .canAlwaysEat(this.canAlwaysEat());
+    }
+
+    @Override
+    public net.minecraft.world.food.FoodProperties getHandle() {
+        return this.impl;
+    }
+
+    static final class BuilderImpl implements FoodProperties.Builder {
+
+        private boolean canAlwaysEat = false;
+        private float saturation = 0;
+        private int nutrition = 0;
+
+        @Override
+        public FoodProperties.Builder canAlwaysEat(final boolean canAlwaysEat) {
+            this.canAlwaysEat = canAlwaysEat;
+            return this;
+        }
+
+        @Override
+        public FoodProperties.Builder saturation(final float saturation) {
+            this.saturation = saturation;
+            return this;
+        }
+
+        @Override
+        public FoodProperties.Builder nutrition(final int nutrition) {
+            Preconditions.checkArgument(nutrition >= 0, "nutrition must be non-negative, was %s", nutrition);
+            this.nutrition = nutrition;
+            return this;
+        }
+
+        @Override
+        public FoodProperties build() {
+            return new PaperFoodProperties(new net.minecraft.world.food.FoodProperties(
+                this.nutrition,
+                this.saturation,
+                this.canAlwaysEat
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6315cd0ebd46f874284c32da9cc03eb77f0677f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java
@@ -0,0 +1,75 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.block.BlockPredicate;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import java.util.Optional;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperItemAdventurePredicate(
+    net.minecraft.world.item.AdventureModePredicate impl
+) implements ItemAdventurePredicate, Handleable<net.minecraft.world.item.AdventureModePredicate> {
+
+    private static List<BlockPredicate> convert(final net.minecraft.world.item.AdventureModePredicate nmsModifiers) {
+        return MCUtil.transformUnmodifiable(nmsModifiers.predicates, nms -> BlockPredicate.predicate()
+            .blocks(nms.blocks().map(blocks -> PaperRegistrySets.convertToApi(RegistryKey.BLOCK, blocks)).orElse(null)).build());
+    }
+
+    @Override
+    public net.minecraft.world.item.AdventureModePredicate getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public PaperItemAdventurePredicate showInTooltip(final boolean showInTooltip) {
+        return new PaperItemAdventurePredicate(this.impl.withTooltip(showInTooltip));
+    }
+
+    @Override
+    public List<BlockPredicate> predicates() {
+        return convert(this.impl);
+    }
+
+    static final class BuilderImpl implements ItemAdventurePredicate.Builder {
+
+        private final List<net.minecraft.advancements.critereon.BlockPredicate> predicates = new ObjectArrayList<>();
+        private boolean showInTooltip = true;
+
+        @Override
+        public ItemAdventurePredicate.Builder addPredicate(final BlockPredicate predicate) {
+            this.predicates.add(new net.minecraft.advancements.critereon.BlockPredicate(Optional.ofNullable(predicate.blocks()).map(
+                blocks -> PaperRegistrySets.convertToNms(Registries.BLOCK, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), blocks)
+            ), Optional.empty(), Optional.empty()));
+            return this;
+        }
+
+        @Override
+        public Builder addPredicates(final List<BlockPredicate> predicates) {
+            for (final BlockPredicate predicate : predicates) {
+                this.addPredicate(predicate);
+            }
+            return this;
+        }
+
+        @Override
+        public ItemAdventurePredicate.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public ItemAdventurePredicate build() {
+            return new PaperItemAdventurePredicate(new net.minecraft.world.item.AdventureModePredicate(new ObjectArrayList<>(this.predicates), this.showInTooltip));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d060c907f4b1bc2bae063ca1e3baf35140215b6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java
@@ -0,0 +1,62 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.inventory.trim.CraftTrimMaterial;
+import org.bukkit.craftbukkit.inventory.trim.CraftTrimPattern;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.meta.trim.ArmorTrim;
+
+public record PaperItemArmorTrim(
+    net.minecraft.world.item.equipment.trim.ArmorTrim impl
+) implements ItemArmorTrim, Handleable<net.minecraft.world.item.equipment.trim.ArmorTrim> {
+
+    @Override
+    public net.minecraft.world.item.equipment.trim.ArmorTrim getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public ItemArmorTrim showInTooltip(final boolean showInTooltip) {
+        return new PaperItemArmorTrim(this.impl.withTooltip(showInTooltip));
+    }
+
+    @Override
+    public ArmorTrim armorTrim() {
+        return new ArmorTrim(CraftTrimMaterial.minecraftHolderToBukkit(this.impl.material()), CraftTrimPattern.minecraftHolderToBukkit(this.impl.pattern()));
+    }
+
+    static final class BuilderImpl implements ItemArmorTrim.Builder {
+
+        private ArmorTrim armorTrim;
+        private boolean showInTooltip = true;
+
+        BuilderImpl(final ArmorTrim armorTrim) {
+            this.armorTrim = armorTrim;
+        }
+
+        @Override
+        public ItemArmorTrim.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public ItemArmorTrim.Builder armorTrim(final ArmorTrim armorTrim) {
+            this.armorTrim = armorTrim;
+            return this;
+        }
+
+        @Override
+        public ItemArmorTrim build() {
+            return new PaperItemArmorTrim(new net.minecraft.world.item.equipment.trim.ArmorTrim(
+                CraftTrimMaterial.bukkitToMinecraftHolder(this.armorTrim.getMaterial()),
+                CraftTrimPattern.bukkitToMinecraftHolder(this.armorTrim.getPattern()),
+                this.showInTooltip
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java
new file mode 100644
index 0000000000000000000000000000000000000000..47ca2b8eb1c1483b6049cf18c7d8a40dd20e7cab
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java
@@ -0,0 +1,97 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeModifier;
+import org.bukkit.craftbukkit.CraftEquipmentSlot;
+import org.bukkit.craftbukkit.attribute.CraftAttribute;
+import org.bukkit.craftbukkit.attribute.CraftAttributeInstance;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.EquipmentSlotGroup;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperItemAttributeModifiers(
+    net.minecraft.world.item.component.ItemAttributeModifiers impl
+) implements ItemAttributeModifiers, Handleable<net.minecraft.world.item.component.ItemAttributeModifiers> {
+
+    private static List<Entry> convert(final net.minecraft.world.item.component.ItemAttributeModifiers nmsModifiers) {
+        return MCUtil.transformUnmodifiable(nmsModifiers.modifiers(), nms -> new PaperEntry(
+            CraftAttribute.minecraftHolderToBukkit(nms.attribute()),
+            CraftAttributeInstance.convert(nms.modifier(), nms.slot())
+        ));
+    }
+
+    @Override
+    public net.minecraft.world.item.component.ItemAttributeModifiers getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public ItemAttributeModifiers showInTooltip(final boolean showInTooltip) {
+        return new PaperItemAttributeModifiers(this.impl.withTooltip(showInTooltip));
+    }
+
+    @Override
+    public @Unmodifiable List<Entry> modifiers() {
+        return convert(this.impl);
+    }
+
+    public record PaperEntry(Attribute attribute, AttributeModifier modifier) implements ItemAttributeModifiers.Entry {
+    }
+
+    static final class BuilderImpl implements ItemAttributeModifiers.Builder {
+
+        private final List<net.minecraft.world.item.component.ItemAttributeModifiers.Entry> entries = new ObjectArrayList<>();
+        private boolean showInTooltip = net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.showInTooltip();
+
+        @Override
+        public Builder addModifier(final Attribute attribute, final AttributeModifier modifier) {
+            return this.addModifier(attribute, modifier, modifier.getSlotGroup());
+        }
+
+        @Override
+        public ItemAttributeModifiers.Builder addModifier(final Attribute attribute, final AttributeModifier modifier, final EquipmentSlotGroup equipmentSlotGroup) {
+            Preconditions.checkArgument(
+                this.entries.stream().noneMatch(e ->
+                    e.modifier().id().equals(CraftNamespacedKey.toMinecraft(modifier.getKey())) && e.attribute().is(CraftNamespacedKey.toMinecraft(attribute.getKey()))
+                ),
+                "Cannot add 2 modifiers with identical keys on the same attribute (modifier %s for attribute %s)",
+                modifier.getKey(), attribute.getKey()
+            );
+
+            this.entries.add(new net.minecraft.world.item.component.ItemAttributeModifiers.Entry(
+                CraftAttribute.bukkitToMinecraftHolder(attribute),
+                CraftAttributeInstance.convert(modifier),
+                CraftEquipmentSlot.getNMSGroup(equipmentSlotGroup)
+            ));
+            return this;
+        }
+
+        @Override
+        public ItemAttributeModifiers.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public ItemAttributeModifiers build() {
+            if (this.entries.isEmpty()) {
+                return new PaperItemAttributeModifiers(net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.withTooltip(this.showInTooltip));
+            }
+
+            return new PaperItemAttributeModifiers(new net.minecraft.world.item.component.ItemAttributeModifiers(
+                new ObjectArrayList<>(this.entries),
+                this.showInTooltip
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c4ecc2d5fc925f245c691facde9c96f3b5eef85
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java
@@ -0,0 +1,65 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemStack;
+
+public record PaperItemContainerContents(
+    net.minecraft.world.item.component.ItemContainerContents impl
+) implements ItemContainerContents, Handleable<net.minecraft.world.item.component.ItemContainerContents> {
+
+    @Override
+    public net.minecraft.world.item.component.ItemContainerContents getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public List<ItemStack> contents() {
+        return MCUtil.transformUnmodifiable(this.impl.items, CraftItemStack::asBukkitCopy);
+    }
+
+    static final class BuilderImpl implements ItemContainerContents.Builder {
+
+        private final List<net.minecraft.world.item.ItemStack> items = new ObjectArrayList<>();
+
+        @Override
+        public ItemContainerContents.Builder add(final ItemStack stack) {
+            Preconditions.checkArgument(stack != null, "Item cannot be null");
+            Preconditions.checkArgument(
+                this.items.size() + 1 <= net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE,
+                "Cannot have more than %s items, had %s",
+                net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE,
+                this.items.size() + 1
+            );
+            this.items.add(CraftItemStack.asNMSCopy(stack));
+            return this;
+        }
+
+        @Override
+        public ItemContainerContents.Builder addAll(final List<ItemStack> stacks) {
+            Preconditions.checkArgument(
+                this.items.size() + stacks.size() <= net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE,
+                "Cannot have more than %s items, had %s",
+                net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE,
+                this.items.size() + stacks.size()
+            );
+            MCUtil.addAndConvert(this.items, stacks, stack -> {
+                Preconditions.checkArgument(stack != null, "Cannot pass null itemstacks!");
+                return CraftItemStack.asNMSCopy(stack);
+            });
+            return this;
+        }
+
+        @Override
+        public ItemContainerContents build() {
+            if (this.items.isEmpty()) {
+                return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.EMPTY);
+            }
+            return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.fromItems(this.items));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cfb18f6a4868ff32e2b118c5833b1b9864e967c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java
@@ -0,0 +1,92 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import net.minecraft.core.Holder;
+import org.bukkit.craftbukkit.enchantments.CraftEnchantment;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.enchantments.Enchantment;
+
+public record PaperItemEnchantments(
+    net.minecraft.world.item.enchantment.ItemEnchantments impl,
+    Map<Enchantment, Integer> enchantments // API values are stored externally as the concept of a lazy key transformer map does not make much sense
+) implements ItemEnchantments, Handleable<net.minecraft.world.item.enchantment.ItemEnchantments> {
+
+    public PaperItemEnchantments(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) {
+        this(itemEnchantments, convert(itemEnchantments));
+    }
+
+    private static Map<Enchantment, Integer> convert(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) {
+        if (itemEnchantments.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        final Map<Enchantment, Integer> map = new HashMap<>(itemEnchantments.size());
+        for (final Object2IntMap.Entry<Holder<net.minecraft.world.item.enchantment.Enchantment>> entry : itemEnchantments.entrySet()) {
+            map.put(CraftEnchantment.minecraftHolderToBukkit(entry.getKey()), entry.getIntValue());
+        }
+        return Collections.unmodifiableMap(map); // TODO look into making a "transforming" map maybe?
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip;
+    }
+
+    @Override
+    public ItemEnchantments showInTooltip(final boolean showInTooltip) {
+        return new PaperItemEnchantments(this.impl.withTooltip(showInTooltip), this.enchantments);
+    }
+
+    @Override
+    public net.minecraft.world.item.enchantment.ItemEnchantments getHandle() {
+        return this.impl;
+    }
+
+    static final class BuilderImpl implements ItemEnchantments.Builder {
+
+        private final Map<Enchantment, Integer> enchantments = new Object2ObjectOpenHashMap<>();
+        private boolean showInTooltip = true;
+
+        @Override
+        public ItemEnchantments.Builder add(final Enchantment enchantment, final int level) {
+            Preconditions.checkArgument(
+                level >= 1 && level <= net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL,
+                "level must be between %s and %s, was %s",
+                1, net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL,
+                level
+            );
+            this.enchantments.put(enchantment, level);
+            return this;
+        }
+
+        @Override
+        public ItemEnchantments.Builder addAll(final Map<Enchantment, Integer> enchantments) {
+            enchantments.forEach(this::add);
+            return this;
+        }
+
+        @Override
+        public ItemEnchantments.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public ItemEnchantments build() {
+            final net.minecraft.world.item.enchantment.ItemEnchantments initialEnchantments = net.minecraft.world.item.enchantment.ItemEnchantments.EMPTY.withTooltip(this.showInTooltip);
+            if (this.enchantments.isEmpty()) {
+                return new PaperItemEnchantments(initialEnchantments);
+            }
+
+            final net.minecraft.world.item.enchantment.ItemEnchantments.Mutable mutable = new net.minecraft.world.item.enchantment.ItemEnchantments.Mutable(initialEnchantments);
+            this.enchantments.forEach((enchantment, level) ->
+                mutable.set(CraftEnchantment.bukkitToMinecraftHolder(enchantment), level)
+            );
+            return new PaperItemEnchantments(mutable.toImmutable());
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java
new file mode 100644
index 0000000000000000000000000000000000000000..3bb0c1aebb03c8dfd6a76ab60c26cbb104586975
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java
@@ -0,0 +1,77 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.ArrayList;
+import java.util.List;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ComponentLike;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperItemLore(
+    net.minecraft.world.item.component.ItemLore impl
+) implements ItemLore, Handleable<net.minecraft.world.item.component.ItemLore> {
+
+    @Override
+    public net.minecraft.world.item.component.ItemLore getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<Component> lines() {
+        return MCUtil.transformUnmodifiable(this.impl.lines(), PaperAdventure::asAdventure);
+    }
+
+    @Override
+    public @Unmodifiable List<Component> styledLines() {
+        return MCUtil.transformUnmodifiable(this.impl.styledLines(), PaperAdventure::asAdventure);
+    }
+
+    static final class BuilderImpl implements ItemLore.Builder {
+
+        private List<Component> lines = new ObjectArrayList<>();
+
+        private static void validateLineCount(final int current, final int add) {
+            final int newSize = current + add;
+            Preconditions.checkArgument(
+                newSize <= net.minecraft.world.item.component.ItemLore.MAX_LINES,
+                "Cannot have more than %s lines, had %s",
+                net.minecraft.world.item.component.ItemLore.MAX_LINES,
+                newSize
+            );
+        }
+
+        @Override
+        public ItemLore.Builder lines(final List<? extends ComponentLike> lines) {
+            validateLineCount(0, lines.size());
+            this.lines = new ArrayList<>(ComponentLike.asComponents(lines));
+            return this;
+        }
+
+        @Override
+        public ItemLore.Builder addLine(final ComponentLike line) {
+            validateLineCount(this.lines.size(), 1);
+            this.lines.add(line.asComponent());
+            return this;
+        }
+
+        @Override
+        public ItemLore.Builder addLines(final List<? extends ComponentLike> lines) {
+            validateLineCount(this.lines.size(), lines.size());
+            this.lines.addAll(ComponentLike.asComponents(lines));
+            return this;
+        }
+
+        @Override
+        public ItemLore build() {
+            if (this.lines.isEmpty()) {
+                return new PaperItemLore(net.minecraft.world.item.component.ItemLore.EMPTY);
+            }
+
+            return new PaperItemLore(new net.minecraft.world.item.component.ItemLore(PaperAdventure.asVanilla(this.lines))); // asVanilla does a list clone
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java
new file mode 100644
index 0000000000000000000000000000000000000000..538a61eaa02c029b4d92f938e0ffde8aa6cf027c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java
@@ -0,0 +1,100 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import net.kyori.adventure.util.TriState;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import org.bukkit.block.BlockType;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperItemTool(
+    net.minecraft.world.item.component.Tool impl
+) implements Tool, Handleable<net.minecraft.world.item.component.Tool> {
+
+    private static List<Tool.Rule> convert(final List<net.minecraft.world.item.component.Tool.Rule> tool) {
+        return MCUtil.transformUnmodifiable(tool, nms -> new PaperRule(
+            PaperRegistrySets.convertToApi(RegistryKey.BLOCK, nms.blocks()),
+            nms.speed().orElse(null),
+            TriState.byBoolean(nms.correctForDrops().orElse(null))
+        ));
+    }
+
+    @Override
+    public net.minecraft.world.item.component.Tool getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<Rule> rules() {
+        return convert(this.impl.rules());
+    }
+
+    @Override
+    public float defaultMiningSpeed() {
+        return this.impl.defaultMiningSpeed();
+    }
+
+    @Override
+    public int damagePerBlock() {
+        return this.impl.damagePerBlock();
+    }
+
+    record PaperRule(RegistryKeySet<BlockType> blocks, @Nullable Float speed, TriState correctForDrops) implements Rule {
+
+        public static PaperRule fromUnsafe(final RegistryKeySet<BlockType> blocks, final @Nullable Float speed, final TriState correctForDrops) {
+            Preconditions.checkArgument(speed == null || speed > 0, "speed must be positive");
+            return new PaperRule(blocks, speed, correctForDrops);
+        }
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private final List<net.minecraft.world.item.component.Tool.Rule> rules = new ObjectArrayList<>();
+        private int damage = 1;
+        private float miningSpeed = 1.0F;
+
+        @Override
+        public Builder damagePerBlock(final int damage) {
+            Preconditions.checkArgument(damage >= 0, "damage must be non-negative, was %s", damage);
+            this.damage = damage;
+            return this;
+        }
+
+        @Override
+        public Builder defaultMiningSpeed(final float miningSpeed) {
+            this.miningSpeed = miningSpeed;
+            return this;
+        }
+
+        @Override
+        public Builder addRule(final Rule rule) {
+            this.rules.add(new net.minecraft.world.item.component.Tool.Rule(
+                PaperRegistrySets.convertToNms(Registries.BLOCK, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), rule.blocks()),
+                Optional.ofNullable(rule.speed()),
+                Optional.ofNullable(rule.correctForDrops().toBoolean())
+            ));
+            return this;
+        }
+
+        @Override
+        public Builder addRules(final Collection<Rule> rules) {
+            rules.forEach(this::addRule);
+            return this;
+        }
+
+        @Override
+        public Tool build() {
+            return new PaperItemTool(new net.minecraft.world.item.component.Tool(new ObjectArrayList<>(this.rules), this.miningSpeed, this.damage));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java
new file mode 100644
index 0000000000000000000000000000000000000000..c43ccf7ccc6157389fce9f9746d5297f0eab1b6e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java
@@ -0,0 +1,62 @@
+package io.papermc.paper.datacomponent.item;
+
+import net.minecraft.world.item.EitherHolder;
+import org.bukkit.JukeboxSong;
+import org.bukkit.craftbukkit.CraftJukeboxSong;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperJukeboxPlayable(
+    net.minecraft.world.item.JukeboxPlayable impl
+) implements JukeboxPlayable, Handleable<net.minecraft.world.item.JukeboxPlayable> {
+
+    @Override
+    public net.minecraft.world.item.JukeboxPlayable getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public PaperJukeboxPlayable showInTooltip(final boolean showInTooltip) {
+        return new PaperJukeboxPlayable(this.impl.withTooltip(showInTooltip));
+    }
+
+    @Override
+    public JukeboxSong jukeboxSong() {
+        return this.impl.song()
+            .unwrap(CraftRegistry.getMinecraftRegistry())
+            .map(CraftJukeboxSong::minecraftHolderToBukkit)
+            .orElseThrow();
+    }
+
+    static final class BuilderImpl implements JukeboxPlayable.Builder {
+
+        private JukeboxSong song;
+        private boolean showInTooltip = true;
+
+        BuilderImpl(final JukeboxSong song) {
+            this.song = song;
+        }
+
+        @Override
+        public JukeboxPlayable.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public JukeboxPlayable.Builder jukeboxSong(final JukeboxSong song) {
+            this.song = song;
+            return this;
+        }
+
+        @Override
+        public JukeboxPlayable build() {
+            return new PaperJukeboxPlayable(new net.minecraft.world.item.JukeboxPlayable(new EitherHolder<>(CraftJukeboxSong.bukkitToMinecraftHolder(this.song)), this.showInTooltip));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b97249f6ae90bc1a10c2089e39f064068d7cd2c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java
@@ -0,0 +1,53 @@
+package io.papermc.paper.datacomponent.item;
+
+import java.util.Optional;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jspecify.annotations.Nullable;
+
+public record PaperLodestoneTracker(
+    net.minecraft.world.item.component.LodestoneTracker impl
+) implements LodestoneTracker, Handleable<net.minecraft.world.item.component.LodestoneTracker> {
+
+    @Override
+    public net.minecraft.world.item.component.LodestoneTracker getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Nullable Location location() {
+        return this.impl.target().map(CraftLocation::fromGlobalPos).orElse(null);
+    }
+
+    @Override
+    public boolean tracked() {
+        return this.impl.tracked();
+    }
+
+    static final class BuilderImpl implements LodestoneTracker.Builder {
+
+        private @Nullable Location location;
+        private boolean tracked = true;
+
+        @Override
+        public LodestoneTracker.Builder location(final @Nullable Location location) {
+            this.location = location;
+            return this;
+        }
+
+        @Override
+        public LodestoneTracker.Builder tracked(final boolean tracked) {
+            this.tracked = tracked;
+            return this;
+        }
+
+        @Override
+        public LodestoneTracker build() {
+            return new PaperLodestoneTracker(new net.minecraft.world.item.component.LodestoneTracker(
+                Optional.ofNullable(this.location).map(CraftLocation::toGlobalPos),
+                this.tracked
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java
new file mode 100644
index 0000000000000000000000000000000000000000..322a1285b0c5127abb67ccab478f1b16b44d0be4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java
@@ -0,0 +1,97 @@
+package io.papermc.paper.datacomponent.item;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import org.bukkit.craftbukkit.map.CraftMapCursor;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.map.MapCursor;
+import org.jspecify.annotations.Nullable;
+
+public record PaperMapDecorations(
+    net.minecraft.world.item.component.MapDecorations impl
+) implements MapDecorations, Handleable<net.minecraft.world.item.component.MapDecorations> {
+
+    @Override
+    public net.minecraft.world.item.component.MapDecorations getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Nullable DecorationEntry decoration(final String id) {
+        final net.minecraft.world.item.component.MapDecorations.Entry decoration = this.impl.decorations().get(id);
+        if (decoration == null) {
+            return null;
+        }
+
+        return new PaperDecorationEntry(decoration);
+    }
+
+    @Override
+    public Map<String, DecorationEntry> decorations() {
+        if (this.impl.decorations().isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        final Set<Map.Entry<String, net.minecraft.world.item.component.MapDecorations.Entry>> entries = this.impl.decorations().entrySet();
+        final Map<String, DecorationEntry> decorations = new Object2ObjectOpenHashMap<>(entries.size());
+        for (final Map.Entry<String, net.minecraft.world.item.component.MapDecorations.Entry> entry : entries) {
+            decorations.put(entry.getKey(), new PaperDecorationEntry(entry.getValue()));
+        }
+
+        return Collections.unmodifiableMap(decorations);
+    }
+
+    public record PaperDecorationEntry(net.minecraft.world.item.component.MapDecorations.Entry entry) implements DecorationEntry {
+
+        public static DecorationEntry toApi(final MapCursor.Type type, final double x, final double z, final float rotation) {
+            return new PaperDecorationEntry(new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(type), x, z, rotation));
+        }
+
+        @Override
+        public MapCursor.Type type() {
+            return CraftMapCursor.CraftType.minecraftHolderToBukkit(this.entry.type());
+        }
+
+        @Override
+        public double x() {
+            return this.entry.x();
+        }
+
+        @Override
+        public double z() {
+            return this.entry.z();
+        }
+
+        @Override
+        public float rotation() {
+            return this.entry.rotation();
+        }
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private final Map<String, net.minecraft.world.item.component.MapDecorations.Entry> entries = new Object2ObjectOpenHashMap<>();
+
+        @Override
+        public MapDecorations.Builder put(final String id, final DecorationEntry entry) {
+            this.entries.put(id, new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(entry.type()), entry.x(), entry.z(), entry.rotation()));
+            return this;
+        }
+
+        @Override
+        public Builder putAll(final Map<String, DecorationEntry> entries) {
+            entries.forEach(this::put);
+            return this;
+        }
+
+        @Override
+        public MapDecorations build() {
+            if (this.entries.isEmpty()) {
+                return new PaperMapDecorations(net.minecraft.world.item.component.MapDecorations.EMPTY);
+            }
+            return new PaperMapDecorations(new net.minecraft.world.item.component.MapDecorations(new Object2ObjectOpenHashMap<>(this.entries)));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2b4cc372bb154bbc741ad1bf47cba210f292c5c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java
@@ -0,0 +1,19 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperMapId(
+    net.minecraft.world.level.saveddata.maps.MapId impl
+) implements MapId, Handleable<net.minecraft.world.level.saveddata.maps.MapId> {
+
+    @Override
+    public net.minecraft.world.level.saveddata.maps.MapId getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public int id() {
+        return this.impl.id();
+    }
+
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b6fdfc9c1248bac426ce24d7b66610a6eff3b8f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java
@@ -0,0 +1,35 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.Color;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperMapItemColor(
+    net.minecraft.world.item.component.MapItemColor impl
+) implements MapItemColor, Handleable<net.minecraft.world.item.component.MapItemColor> {
+
+    @Override
+    public net.minecraft.world.item.component.MapItemColor getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public Color color() {
+        return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private Color color = Color.fromRGB(net.minecraft.world.item.component.MapItemColor.DEFAULT.rgb());
+
+        @Override
+        public Builder color(final Color color) {
+            this.color = color;
+            return this;
+        }
+
+        @Override
+        public MapItemColor build() {
+            return new PaperMapItemColor(new net.minecraft.world.item.component.MapItemColor(this.color.asRGB()));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..a7ed2aa21d0384384a4c5830ead544cb064b15b6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperOminousBottleAmplifier(
+    net.minecraft.world.item.component.OminousBottleAmplifier impl
+) implements OminousBottleAmplifier, Handleable<net.minecraft.world.item.component.OminousBottleAmplifier> {
+
+    @Override
+    public net.minecraft.world.item.component.OminousBottleAmplifier getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public int amplifier() {
+        return this.impl.value();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java
new file mode 100644
index 0000000000000000000000000000000000000000..bde757b51d0ae6a36870c789d416ec0e05c4cadf
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java
@@ -0,0 +1,83 @@
+package io.papermc.paper.datacomponent.item;
+
+import java.util.Optional;
+import org.bukkit.craftbukkit.inventory.CraftItemType;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemType;
+import org.jspecify.annotations.Nullable;
+
+public record PaperPotDecorations(
+    net.minecraft.world.level.block.entity.PotDecorations impl
+) implements PotDecorations, Handleable<net.minecraft.world.level.block.entity.PotDecorations> {
+
+    @Override
+    public @Nullable ItemType back() {
+        return this.impl.back().map(CraftItemType::minecraftToBukkitNew).orElse(null);
+    }
+
+    @Override
+    public @Nullable ItemType left() {
+        return this.impl.left().map(CraftItemType::minecraftToBukkitNew).orElse(null);
+    }
+
+    @Override
+    public @Nullable ItemType right() {
+        return this.impl.right().map(CraftItemType::minecraftToBukkitNew).orElse(null);
+    }
+
+    @Override
+    public @Nullable ItemType front() {
+        return this.impl.front().map(CraftItemType::minecraftToBukkitNew).orElse(null);
+    }
+
+    @Override
+    public net.minecraft.world.level.block.entity.PotDecorations getHandle() {
+        return this.impl;
+    }
+
+    static final class BuilderImpl implements PotDecorations.Builder {
+
+        private @Nullable ItemType back;
+        private @Nullable ItemType left;
+        private @Nullable ItemType right;
+        private @Nullable ItemType front;
+
+        @Override
+        public PotDecorations.Builder back(final @Nullable ItemType back) {
+            this.back = back;
+            return this;
+        }
+
+        @Override
+        public PotDecorations.Builder left(final @Nullable ItemType left) {
+            this.left = left;
+            return this;
+        }
+
+        @Override
+        public PotDecorations.Builder right(final @Nullable ItemType right) {
+            this.right = right;
+            return this;
+        }
+
+        @Override
+        public PotDecorations.Builder front(final @Nullable ItemType front) {
+            this.front = front;
+            return this;
+        }
+
+        @Override
+        public PotDecorations build() {
+            if (this.back == null && this.left == null && this.right == null && this.front == null) {
+                return new PaperPotDecorations(net.minecraft.world.level.block.entity.PotDecorations.EMPTY);
+            }
+
+            return new PaperPotDecorations(new net.minecraft.world.level.block.entity.PotDecorations(
+                Optional.ofNullable(this.back).map(CraftItemType::bukkitToMinecraftNew),
+                Optional.ofNullable(this.left).map(CraftItemType::bukkitToMinecraftNew),
+                Optional.ofNullable(this.right).map(CraftItemType::bukkitToMinecraftNew),
+                Optional.ofNullable(this.front).map(CraftItemType::bukkitToMinecraftNew)
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java
new file mode 100644
index 0000000000000000000000000000000000000000..4712f8bbaa9f00ede895651472d7975ffa30c88d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java
@@ -0,0 +1,103 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import java.util.Optional;
+import net.minecraft.world.effect.MobEffectInstance;
+import org.bukkit.Color;
+import org.bukkit.craftbukkit.potion.CraftPotionType;
+import org.bukkit.craftbukkit.potion.CraftPotionUtil;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionType;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperPotionContents(
+    net.minecraft.world.item.alchemy.PotionContents impl
+) implements PotionContents, Handleable<net.minecraft.world.item.alchemy.PotionContents> {
+
+    @Override
+    public net.minecraft.world.item.alchemy.PotionContents getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<PotionEffect> customEffects() {
+        return MCUtil.transformUnmodifiable(this.impl.customEffects(), CraftPotionUtil::toBukkit);
+    }
+
+    @Override
+    public @Nullable PotionType potion() {
+        return this.impl.potion()
+            .map(CraftPotionType::minecraftHolderToBukkit)
+            .orElse(null);
+    }
+
+    @Override
+    public @Nullable Color customColor() {
+        return this.impl.customColor()
+            .map(Color::fromARGB) // alpha channel is supported for tipped arrows, so let's just leave it in
+            .orElse(null);
+    }
+
+    @Override
+    public @Nullable String customName() {
+        return this.impl.customName().orElse(null);
+    }
+
+    static final class BuilderImpl implements PotionContents.Builder {
+
+        private final List<MobEffectInstance> customEffects = new ObjectArrayList<>();
+        private @Nullable PotionType type;
+        private @Nullable Color color;
+        private @Nullable String customName;
+
+        @Override
+        public PotionContents.Builder potion(final @Nullable PotionType type) {
+            this.type = type;
+            return this;
+        }
+
+        @Override
+        public PotionContents.Builder customColor(final @Nullable Color color) {
+            this.color = color;
+            return this;
+        }
+
+        @Override
+        public Builder customName(final @Nullable String name) {
+            Preconditions.checkArgument(name == null || name.length() <= Short.MAX_VALUE, "Custom name is longer than %s characters", Short.MAX_VALUE);
+            this.customName = name;
+            return this;
+        }
+
+        @Override
+        public PotionContents.Builder addCustomEffect(final PotionEffect effect) {
+            this.customEffects.add(CraftPotionUtil.fromBukkit(effect));
+            return this;
+        }
+
+        @Override
+        public PotionContents.Builder addCustomEffects(final List<PotionEffect> effects) {
+            effects.forEach(this::addCustomEffect);
+            return this;
+        }
+
+        @Override
+        public PotionContents build() {
+            if (this.type == null && this.color == null && this.customEffects.isEmpty() && this.customName == null) {
+                return new PaperPotionContents(net.minecraft.world.item.alchemy.PotionContents.EMPTY);
+            }
+
+            return new PaperPotionContents(new net.minecraft.world.item.alchemy.PotionContents(
+                Optional.ofNullable(this.type).map(CraftPotionType::bukkitToMinecraftHolder),
+                Optional.ofNullable(this.color).map(Color::asARGB),
+                new ObjectArrayList<>(this.customEffects),
+                Optional.ofNullable(this.customName)
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java
new file mode 100644
index 0000000000000000000000000000000000000000..96345e051c4aa77820e857a02768b684d52d7096
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java
@@ -0,0 +1,22 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemType;
+
+public record PaperRepairable(
+    net.minecraft.world.item.enchantment.Repairable impl
+) implements Repairable, Handleable<net.minecraft.world.item.enchantment.Repairable> {
+
+    @Override
+    public net.minecraft.world.item.enchantment.Repairable getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public RegistryKeySet<ItemType> types() {
+        return PaperRegistrySets.convertToApi(RegistryKey.ITEM, this.impl.items());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..7583a7efb4bfdb0157ee27a1b7cfb661eeccb02a
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java
@@ -0,0 +1,105 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.destroystokyo.paper.profile.CraftPlayerProfile;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.destroystokyo.paper.profile.ProfileProperty;
+import com.google.common.base.Preconditions;
+import com.mojang.authlib.properties.Property;
+import com.mojang.authlib.properties.PropertyMap;
+import io.papermc.paper.util.MCUtil;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import net.minecraft.util.StringUtil;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperResolvableProfile(
+    net.minecraft.world.item.component.ResolvableProfile impl
+) implements ResolvableProfile, Handleable<net.minecraft.world.item.component.ResolvableProfile> {
+
+    static PaperResolvableProfile toApi(final PlayerProfile profile) {
+        return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile(CraftPlayerProfile.asAuthlibCopy(profile)));
+    }
+
+    @Override
+    public net.minecraft.world.item.component.ResolvableProfile getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Nullable UUID uuid() {
+        return this.impl.id().orElse(null);
+    }
+
+    @Override
+    public @Nullable String name() {
+        return this.impl.name().orElse(null);
+    }
+
+    @Override
+    public @Unmodifiable Collection<ProfileProperty> properties() {
+        return MCUtil.transformUnmodifiable(this.impl.properties().values(), input -> new ProfileProperty(input.name(), input.value(), input.signature()));
+    }
+
+    @Override
+    public CompletableFuture<PlayerProfile> resolve() {
+        return this.impl.resolve().thenApply(resolvableProfile -> CraftPlayerProfile.asBukkitCopy(resolvableProfile.gameProfile()));
+    }
+
+    static final class BuilderImpl implements ResolvableProfile.Builder {
+
+        private final PropertyMap propertyMap = new PropertyMap();
+        private @Nullable String name;
+        private @Nullable UUID uuid;
+
+        @Override
+        public ResolvableProfile.Builder name(final @Nullable String name) {
+            if (name != null) {
+                Preconditions.checkArgument(name.length() <= 16, "name cannot be more than 16 characters, was %s", name.length());
+                Preconditions.checkArgument(StringUtil.isValidPlayerName(name), "name cannot include invalid characters, was %s", name);
+            }
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public ResolvableProfile.Builder uuid(final @Nullable UUID uuid) {
+            this.uuid = uuid;
+            return this;
+        }
+
+        @Override
+        public ResolvableProfile.Builder addProperty(final ProfileProperty property) {
+            // ProfileProperty constructor already has specific validations
+            final Property newProperty = new Property(property.getName(), property.getValue(), property.getSignature());
+            if (!this.propertyMap.containsEntry(property.getName(), newProperty)) { // underlying map is a multimap that doesn't allow duplicate key-value pair
+                final int newSize = this.propertyMap.size() + 1;
+                Preconditions.checkArgument(newSize <= 16, "Cannot have more than 16 properties, was %s", newSize);
+            }
+
+            this.propertyMap.put(property.getName(), newProperty);
+            return this;
+        }
+
+        @Override
+        public ResolvableProfile.Builder addProperties(final Collection<ProfileProperty> properties) {
+            properties.forEach(this::addProperty);
+            return this;
+        }
+
+        @Override
+        public ResolvableProfile build() {
+            final PropertyMap shallowCopy = new PropertyMap();
+            shallowCopy.putAll(this.propertyMap);
+
+            return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile(
+                Optional.ofNullable(this.name),
+                Optional.ofNullable(this.uuid),
+                shallowCopy
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ee469b3b690a877e066dbca79706678cd915fa8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java
@@ -0,0 +1,59 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.key.Key;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.storage.loot.LootTable;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperSeededContainerLoot(
+    net.minecraft.world.item.component.SeededContainerLoot impl
+) implements SeededContainerLoot, Handleable<net.minecraft.world.item.component.SeededContainerLoot> {
+
+    @Override
+    public net.minecraft.world.item.component.SeededContainerLoot getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public Key lootTable() {
+        return CraftNamespacedKey.fromMinecraft(this.impl.lootTable().location());
+    }
+
+    @Override
+    public long seed() {
+        return this.impl.seed();
+    }
+
+    static final class BuilderImpl implements SeededContainerLoot.Builder {
+
+        private long seed = LootTable.RANDOMIZE_SEED;
+        private Key key;
+
+        BuilderImpl(final Key key) {
+            this.key = key;
+        }
+
+        @Override
+        public SeededContainerLoot.Builder lootTable(final Key key) {
+            this.key = key;
+            return this;
+        }
+
+        @Override
+        public SeededContainerLoot.Builder seed(final long seed) {
+            this.seed = seed;
+            return this;
+        }
+
+        @Override
+        public SeededContainerLoot build() {
+            return new PaperSeededContainerLoot(new net.minecraft.world.item.component.SeededContainerLoot(
+                ResourceKey.create(Registries.LOOT_TABLE, PaperAdventure.asVanilla(this.key)),
+                this.seed
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java
new file mode 100644
index 0000000000000000000000000000000000000000..41df23c7e7e713e88eef757fda347381e151b0fc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java
@@ -0,0 +1,58 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.potion.SuspiciousEffectEntry;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.bukkit.craftbukkit.potion.CraftPotionEffectType;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+import static io.papermc.paper.potion.SuspiciousEffectEntry.create;
+
+public record PaperSuspiciousStewEffects(
+    net.minecraft.world.item.component.SuspiciousStewEffects impl
+) implements SuspiciousStewEffects, Handleable<net.minecraft.world.item.component.SuspiciousStewEffects> {
+
+    @Override
+    public net.minecraft.world.item.component.SuspiciousStewEffects getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<SuspiciousEffectEntry> effects() {
+        return MCUtil.transformUnmodifiable(this.impl.effects(), entry -> create(CraftPotionEffectType.minecraftHolderToBukkit(entry.effect()), entry.duration()));
+    }
+
+    static final class BuilderImpl implements Builder {
+
+        private final List<net.minecraft.world.item.component.SuspiciousStewEffects.Entry> effects = new ObjectArrayList<>();
+
+        @Override
+        public Builder add(final SuspiciousEffectEntry entry) {
+            this.effects.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry(
+                org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(entry.effect()),
+                entry.duration()
+            ));
+            return this;
+        }
+
+        @Override
+        public Builder addAll(final Collection<SuspiciousEffectEntry> entries) {
+            entries.forEach(this::add);
+            return this;
+        }
+
+        @Override
+        public SuspiciousStewEffects build() {
+            if (this.effects.isEmpty()) {
+                return new PaperSuspiciousStewEffects(net.minecraft.world.item.component.SuspiciousStewEffects.EMPTY);
+            }
+
+            return new PaperSuspiciousStewEffects(
+                new net.minecraft.world.item.component.SuspiciousStewEffects(new ObjectArrayList<>(this.effects))
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java
new file mode 100644
index 0000000000000000000000000000000000000000..edeb3308af4c359d1930fdbc5417727451b6f0eb
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java
@@ -0,0 +1,39 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.util.Handleable;
+
+public record PaperUnbreakable(
+    net.minecraft.world.item.component.Unbreakable impl
+) implements Unbreakable, Handleable<net.minecraft.world.item.component.Unbreakable> {
+
+    @Override
+    public boolean showInTooltip() {
+        return this.impl.showInTooltip();
+    }
+
+    @Override
+    public Unbreakable showInTooltip(final boolean showInTooltip) {
+        return new PaperUnbreakable(this.impl.withTooltip(showInTooltip));
+    }
+
+    @Override
+    public net.minecraft.world.item.component.Unbreakable getHandle() {
+        return this.impl;
+    }
+
+    static final class BuilderImpl implements Unbreakable.Builder {
+
+        private boolean showInTooltip = true;
+
+        @Override
+        public Unbreakable.Builder showInTooltip(final boolean showInTooltip) {
+            this.showInTooltip = showInTooltip;
+            return this;
+        }
+
+        @Override
+        public Unbreakable build() {
+            return new PaperUnbreakable(new net.minecraft.world.item.component.Unbreakable(this.showInTooltip));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java
new file mode 100644
index 0000000000000000000000000000000000000000..1aeab920faaf5653ddb8e77372060fb8d3226641
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java
@@ -0,0 +1,56 @@
+package io.papermc.paper.datacomponent.item;
+
+import io.papermc.paper.adventure.PaperAdventure;
+import java.util.Optional;
+import net.kyori.adventure.key.Key;
+import net.minecraft.resources.ResourceLocation;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jspecify.annotations.Nullable;
+
+public record PaperUseCooldown(
+    net.minecraft.world.item.component.UseCooldown impl
+) implements UseCooldown, Handleable<net.minecraft.world.item.component.UseCooldown> {
+
+    @Override
+    public net.minecraft.world.item.component.UseCooldown getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public float seconds() {
+        return this.impl.seconds();
+    }
+
+    @Override
+    public @Nullable Key cooldownGroup() {
+        return this.impl.cooldownGroup()
+            .map(PaperAdventure::asAdventure)
+            .orElse(null);
+    }
+
+
+    static final class BuilderImpl implements Builder {
+
+        private final float seconds;
+        private Optional<ResourceLocation> cooldownGroup = Optional.empty();
+
+        BuilderImpl(final float seconds) {
+            this.seconds = seconds;
+        }
+
+        @Override
+        public Builder cooldownGroup(@Nullable final Key key) {
+            this.cooldownGroup = Optional.ofNullable(key)
+                .map(PaperAdventure::asVanilla);
+
+            return this;
+        }
+
+        @Override
+        public UseCooldown build() {
+            return new PaperUseCooldown(
+                new net.minecraft.world.item.component.UseCooldown(this.seconds, this.cooldownGroup)
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2c04506940704c2ec9a5e6bb469c4771e2d49c2
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java
@@ -0,0 +1,20 @@
+package io.papermc.paper.datacomponent.item;
+
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.bukkit.inventory.ItemStack;
+
+public record PaperUseRemainder(
+    net.minecraft.world.item.component.UseRemainder impl
+) implements UseRemainder, Handleable<net.minecraft.world.item.component.UseRemainder> {
+
+    @Override
+    public net.minecraft.world.item.component.UseRemainder getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public ItemStack transformInto() {
+        return CraftItemStack.asBukkitCopy(this.impl.convertInto());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java
new file mode 100644
index 0000000000000000000000000000000000000000..559343a33bada475cc5bbcd431cd88b537c8cef7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java
@@ -0,0 +1,103 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.text.Filtered;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import java.util.Optional;
+import net.minecraft.server.network.Filterable;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+public record PaperWritableBookContent(
+    net.minecraft.world.item.component.WritableBookContent impl
+) implements WritableBookContent, Handleable<net.minecraft.world.item.component.WritableBookContent> {
+
+    @Override
+    public net.minecraft.world.item.component.WritableBookContent getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public @Unmodifiable List<Filtered<String>> pages() {
+        return MCUtil.transformUnmodifiable(this.impl.pages(), input -> Filtered.of(input.raw(), input.filtered().orElse(null)));
+    }
+
+    static final class BuilderImpl implements WritableBookContent.Builder {
+
+        private final List<Filterable<String>> pages = new ObjectArrayList<>();
+
+        private static void validatePageLength(final String page) {
+            Preconditions.checkArgument(
+                page.length() <= net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH,
+                "Cannot have page length more than %s, had %s",
+                net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH,
+                page.length()
+            );
+        }
+
+        private static void validatePageCount(final int current, final int add) {
+            final int newSize = current + add;
+            Preconditions.checkArgument(
+                newSize <= net.minecraft.world.item.component.WritableBookContent.MAX_PAGES,
+                "Cannot have more than %s pages, had %s",
+                net.minecraft.world.item.component.WritableBookContent.MAX_PAGES,
+                newSize
+            );
+        }
+
+        @Override
+        public WritableBookContent.Builder addPage(final String page) {
+            validatePageLength(page);
+            validatePageCount(this.pages.size(), 1);
+            this.pages.add(Filterable.passThrough(page));
+            return this;
+        }
+
+        @Override
+        public WritableBookContent.Builder addPages(final List<String> pages) {
+            validatePageCount(this.pages.size(), pages.size());
+            for (final String page : pages) {
+                validatePageLength(page);
+                this.pages.add(Filterable.passThrough(page));
+            }
+            return this;
+        }
+
+        @Override
+        public WritableBookContent.Builder addFilteredPage(final Filtered<String> page) {
+            validatePageLength(page.raw());
+            if (page.filtered() != null) {
+                validatePageLength(page.filtered());
+            }
+            validatePageCount(this.pages.size(), 1);
+            this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered())));
+            return this;
+        }
+
+        @Override
+        public WritableBookContent.Builder addFilteredPages(final List<Filtered<String>> pages) {
+            validatePageCount(this.pages.size(), pages.size());
+            for (final Filtered<String> page : pages) {
+                validatePageLength(page.raw());
+                if (page.filtered() != null) {
+                    validatePageLength(page.filtered());
+                }
+                this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered())));
+            }
+            return this;
+        }
+
+        @Override
+        public WritableBookContent build() {
+            if (this.pages.isEmpty()) {
+                return new PaperWritableBookContent(net.minecraft.world.item.component.WritableBookContent.EMPTY);
+            }
+
+            return new PaperWritableBookContent(
+                new net.minecraft.world.item.component.WritableBookContent(new ObjectArrayList<>(this.pages))
+            );
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java
new file mode 100644
index 0000000000000000000000000000000000000000..037a6695bdb8ee6e3c119fa79000c4ea28e1bef8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java
@@ -0,0 +1,183 @@
+package io.papermc.paper.datacomponent.item;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.text.Filtered;
+import io.papermc.paper.util.MCUtil;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import java.util.List;
+import java.util.Optional;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ComponentLike;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.server.network.Filterable;
+import net.minecraft.util.GsonHelper;
+import org.bukkit.craftbukkit.util.Handleable;
+import org.jetbrains.annotations.Unmodifiable;
+
+import static io.papermc.paper.adventure.PaperAdventure.asAdventure;
+import static io.papermc.paper.adventure.PaperAdventure.asVanilla;
+
+public record PaperWrittenBookContent(
+    net.minecraft.world.item.component.WrittenBookContent impl
+) implements WrittenBookContent, Handleable<net.minecraft.world.item.component.WrittenBookContent> {
+
+    @Override
+    public net.minecraft.world.item.component.WrittenBookContent getHandle() {
+        return this.impl;
+    }
+
+    @Override
+    public Filtered<String> title() {
+        return Filtered.of(this.impl.title().raw(), this.impl.title().filtered().orElse(null));
+    }
+
+    @Override
+    public String author() {
+        return this.impl.author();
+    }
+
+    @Override
+    public int generation() {
+        return this.impl.generation();
+    }
+
+    @Override
+    public @Unmodifiable List<Filtered<Component>> pages() {
+        return MCUtil.transformUnmodifiable(
+            this.impl.pages(),
+            page -> Filtered.of(asAdventure(page.raw()), page.filtered().map(PaperAdventure::asAdventure).orElse(null))
+        );
+    }
+
+    @Override
+    public boolean resolved() {
+        return this.impl.resolved();
+    }
+
+    static final class BuilderImpl implements WrittenBookContent.Builder {
+
+        private final List<Filterable<net.minecraft.network.chat.Component>> pages = new ObjectArrayList<>();
+        private Filterable<String> title;
+        private String author;
+        private int generation = 0;
+        private boolean resolved = false;
+
+        BuilderImpl(final Filtered<String> title, final String author) {
+            validateTitle(title.raw());
+            if (title.filtered() != null) {
+                validateTitle(title.filtered());
+            }
+            this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered()));
+            this.author = author;
+        }
+
+        private static void validateTitle(final String title) {
+            Preconditions.checkArgument(
+                title.length() <= net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH,
+                "Title cannot be longer than %s, was %s",
+                net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH,
+                title.length()
+            );
+        }
+
+        private static void validatePageLength(final Component page) {
+            final String flagPage = GsonHelper.toStableString(GsonComponentSerializer.gson().serializeToTree(page));
+            Preconditions.checkArgument(
+                flagPage.length() <= net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH,
+                "Cannot have page length more than %s, had %s",
+                net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH,
+                flagPage.length()
+            );
+        }
+
+        @Override
+        public WrittenBookContent.Builder title(final String title) {
+            validateTitle(title);
+            this.title = Filterable.passThrough(title);
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder filteredTitle(final Filtered<String> title) {
+            validateTitle(title.raw());
+            if (title.filtered() != null) {
+                validateTitle(title.filtered());
+            }
+            this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered()));
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder author(final String author) {
+            this.author = author;
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder generation(final int generation) {
+            Preconditions.checkArgument(
+                generation >= 0 && generation <= net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION,
+                "generation must be between %s and %s, was %s",
+                0, net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION,
+                generation
+            );
+            this.generation = generation;
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder resolved(final boolean resolved) {
+            this.resolved = resolved;
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder addPage(final ComponentLike page) {
+            final Component component = page.asComponent();
+            validatePageLength(component);
+            this.pages.add(Filterable.passThrough(asVanilla(component)));
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder addPages(final List<? extends ComponentLike> pages) {
+            for (final ComponentLike page : pages) {
+                final Component component = page.asComponent();
+                validatePageLength(component);
+                this.pages.add(Filterable.passThrough(asVanilla(component)));
+            }
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder addFilteredPage(final Filtered<? extends ComponentLike> page) {
+            final Component raw = page.raw().asComponent();
+            validatePageLength(raw);
+            Component filtered = null;
+            if (page.filtered() != null) {
+                filtered = page.filtered().asComponent();
+                validatePageLength(filtered);
+            }
+            this.pages.add(new Filterable<>(asVanilla(raw), Optional.ofNullable(filtered).map(PaperAdventure::asVanilla)));
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent.Builder addFilteredPages(final List<Filtered<? extends ComponentLike>> pages) {
+            pages.forEach(this::addFilteredPage);
+            return this;
+        }
+
+        @Override
+        public WrittenBookContent build() {
+            return new PaperWrittenBookContent(new net.minecraft.world.item.component.WrittenBookContent(
+                this.title,
+                this.author,
+                this.generation,
+                new ObjectArrayList<>(this.pages),
+                this.resolved
+            ));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..eab1883d691e0d0034b7959c8130a6240c3f529c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java
@@ -0,0 +1,64 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import java.util.ArrayList;
+import java.util.List;
+import net.kyori.adventure.key.Key;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import org.bukkit.craftbukkit.potion.CraftPotionUtil;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+
+@ApiStatus.Internal
+@NullMarked
+public class ConsumableTypesBridgeImpl implements ConsumableTypesBridge {
+
+    @Override
+    public ConsumeEffect.ApplyStatusEffects applyStatusEffects(final List<PotionEffect> effectList, final float probability) {
+        Preconditions.checkArgument(0 <= probability && probability <= 1, "probability must be between 0-1, was %s", probability);
+        return new PaperApplyStatusEffects(
+            new net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect(
+                new ArrayList<>(Lists.transform(effectList, CraftPotionUtil::fromBukkit)),
+                probability
+            )
+        );
+    }
+
+    @Override
+    public ConsumeEffect.RemoveStatusEffects removeStatusEffects(final RegistryKeySet<PotionEffectType> effectTypes) {
+        return new PaperRemoveStatusEffects(
+            new net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect(
+                PaperRegistrySets.convertToNms(Registries.MOB_EFFECT, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), effectTypes)
+            )
+        );
+    }
+
+    @Override
+    public ConsumeEffect.ClearAllStatusEffects clearAllStatusEffects() {
+        return new PaperClearAllStatusEffects(
+            new net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect()
+        );
+    }
+
+    @Override
+    public ConsumeEffect.PlaySound playSoundEffect(final Key sound) {
+        return new PaperPlaySound(
+            new net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect(PaperAdventure.resolveSound(sound))
+        );
+    }
+
+    @Override
+    public ConsumeEffect.TeleportRandomly teleportRandomlyEffect(final float diameter) {
+        Preconditions.checkArgument(diameter > 0, "diameter must be positive, was %s", diameter);
+        return new PaperTeleportRandomly(
+            new net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect(diameter)
+        );
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d2a4ba560f5a34139517ac2e17667c94a3c56f6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java
@@ -0,0 +1,28 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import java.util.List;
+import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect;
+import org.bukkit.craftbukkit.potion.CraftPotionUtil;
+import org.bukkit.potion.PotionEffect;
+
+import static io.papermc.paper.util.MCUtil.transformUnmodifiable;
+
+public record PaperApplyStatusEffects(
+    ApplyStatusEffectsConsumeEffect impl
+) implements ConsumeEffect.ApplyStatusEffects, PaperConsumableEffectImpl<ApplyStatusEffectsConsumeEffect> {
+
+    @Override
+    public List<PotionEffect> effects() {
+        return transformUnmodifiable(this.impl().effects(), CraftPotionUtil::toBukkit);
+    }
+
+    @Override
+    public float probability() {
+        return this.impl.probability();
+    }
+
+    @Override
+    public ApplyStatusEffectsConsumeEffect getHandle() {
+        return this.impl;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java
new file mode 100644
index 0000000000000000000000000000000000000000..2afcbbbeb486783737fd606113b6f938d0a18cb5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java
@@ -0,0 +1,11 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+public record PaperClearAllStatusEffects(
+    net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect impl
+) implements ConsumeEffect.ClearAllStatusEffects, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect> {
+
+    @Override
+    public net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect getHandle() {
+        return this.impl;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..05ede1d3f5b0b5ea3a5004cb4a7a153ed7714a55
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java
@@ -0,0 +1,7 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import net.minecraft.world.item.consume_effects.ConsumeEffect;
+import org.bukkit.craftbukkit.util.Handleable;
+
+public interface PaperConsumableEffectImpl<T extends ConsumeEffect> extends Handleable<T> {
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff07939ef0730a11c712c09c360da8a21a777618
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java
@@ -0,0 +1,32 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect;
+import net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect;
+import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect;
+import net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect;
+import net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect;
+
+public final class PaperConsumableEffects {
+
+    private PaperConsumableEffects() {
+    }
+
+    public static ConsumeEffect fromNms(net.minecraft.world.item.consume_effects.ConsumeEffect consumable) {
+        return switch (consumable) {
+            case ApplyStatusEffectsConsumeEffect effect -> new PaperApplyStatusEffects(effect);
+            case ClearAllStatusEffectsConsumeEffect effect -> new PaperClearAllStatusEffects(effect);
+            case PlaySoundConsumeEffect effect -> new PaperPlaySound(effect);
+            case RemoveStatusEffectsConsumeEffect effect -> new PaperRemoveStatusEffects(effect);
+            case TeleportRandomlyConsumeEffect effect -> new PaperTeleportRandomly(effect);
+            default -> throw new UnsupportedOperationException("Don't know how to convert " + consumable.getClass());
+        };
+    }
+
+    public static net.minecraft.world.item.consume_effects.ConsumeEffect toNms(ConsumeEffect effect) {
+        if (effect instanceof PaperConsumableEffectImpl<?> consumableEffect) {
+            return consumableEffect.getHandle();
+        } else {
+            throw new UnsupportedOperationException("Must implement handleable!");
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java
new file mode 100644
index 0000000000000000000000000000000000000000..26a8ee292b45e57462e6e6629b328fbf9d6b47e7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java
@@ -0,0 +1,20 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.key.Key;
+import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect;
+
+public record PaperPlaySound(
+    PlaySoundConsumeEffect impl
+) implements ConsumeEffect.PlaySound, PaperConsumableEffectImpl<PlaySoundConsumeEffect> {
+
+    @Override
+    public Key sound() {
+        return PaperAdventure.asAdventure(this.impl.sound().value().location());
+    }
+
+    @Override
+    public PlaySoundConsumeEffect getHandle() {
+        return this.impl;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java
new file mode 100644
index 0000000000000000000000000000000000000000..20e09c6ebab91b1ec103aa149d0f57a2a5502644
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.PaperRegistrySets;
+import io.papermc.paper.registry.set.RegistryKeySet;
+import org.bukkit.potion.PotionEffectType;
+
+public record PaperRemoveStatusEffects(
+    net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect impl
+) implements ConsumeEffect.RemoveStatusEffects, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect> {
+
+    @Override
+    public RegistryKeySet<PotionEffectType> removeEffects() {
+        return PaperRegistrySets.convertToApi(RegistryKey.MOB_EFFECT, this.impl.effects());
+    }
+
+    @Override
+    public net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect getHandle() {
+        return this.impl;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java
new file mode 100644
index 0000000000000000000000000000000000000000..c21889e9984f7c36d9f19771c2e23b6efba5197d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java
@@ -0,0 +1,15 @@
+package io.papermc.paper.datacomponent.item.consumable;
+
+public record PaperTeleportRandomly(
+    net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect impl
+) implements ConsumeEffect.TeleportRandomly, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect> {
+    @Override
+    public float diameter() {
+        return this.impl.diameter();
+    }
+
+    @Override
+    public net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect getHandle() {
+        return this.impl;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..af6720a49a9d336a345e2bc91d6714f6b2c39886
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Relating to consumable effects for components.
+ */
+@NullMarked
+package io.papermc.paper.datacomponent.item.consumable;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/main/java/io/papermc/paper/datacomponent/item/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..02a69025662d6a887f5449fd5eaf7d1083973bf3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package io.papermc.paper.datacomponent.item;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/main/java/io/papermc/paper/datacomponent/package-info.java b/src/main/java/io/papermc/paper/datacomponent/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..62aa1061c35d5358e6dec16a52574b427cc4b732
--- /dev/null
+++ b/src/main/java/io/papermc/paper/datacomponent/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package io.papermc.paper.datacomponent;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
index fd024576e70e0c121c1477a0b7777af18159b7c4..b81d0d906789ef2ff3759395821316462aacf323 100644
--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
@@ -2,6 +2,8 @@ package io.papermc.paper.registry;
 
 import com.google.common.base.Preconditions;
 import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.datacomponent.DataComponentType;
+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.data.PaperPaintingVariantRegistryEntry;
@@ -95,6 +97,7 @@ public final class PaperRegistries {
             entry(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE, Attribute.class, CraftAttribute::new),
             entry(Registries.FLUID, RegistryKey.FLUID, Fluid.class, CraftFluid::new),
             entry(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT, Sound.class, CraftSound::new),
+            entry(Registries.DATA_COMPONENT_TYPE, RegistryKey.DATA_COMPONENT_TYPE, DataComponentType.class, PaperComponentType::of),
 
             // data-drivens
             entry(Registries.BIOME, RegistryKey.BIOME, Biome.class, CraftBiome::new).delayed(),
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
index 756c73a401437566258813946fa10c7caa8f2469..ccad5a7018f6eaacb011818e9da990c75e0d06df 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
@@ -206,7 +206,7 @@ public final class CraftItemStack extends ItemStack {
                 this.adjustTagForItemMeta(oldType); // Paper
             }
         }
-        this.setData(null);
+        this.setData((MaterialData) null); // Paper
     }
 
     @Override
@@ -245,7 +245,7 @@ public final class CraftItemStack extends ItemStack {
 
     @Override
     public int getMaxStackSize() {
-        return (this.handle == null) ? Material.AIR.getMaxStackSize() : this.handle.getMaxStackSize();
+        return (this.handle == null) ? Item.DEFAULT_MAX_STACK_SIZE : this.handle.getMaxStackSize(); // Paper - air stacks to 64
     }
 
     // Paper start
@@ -267,12 +267,14 @@ public final class CraftItemStack extends ItemStack {
     public void addUnsafeEnchantment(Enchantment ench, int level) {
         Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
 
-        // Paper start - Replace whole method
-        final ItemMeta itemMeta = this.getItemMeta();
-        if (itemMeta != null) {
-            itemMeta.addEnchant(ench, level, true);
-            this.setItemMeta(itemMeta);
+        // Paper start
+        if (this.handle == null) {
+            return;
         }
+
+        EnchantmentHelper.updateEnchantments(this.handle, mutable -> { // data component api doesn't really support mutable things once already set yet
+            mutable.set(CraftEnchantment.bukkitToMinecraftHolder(ench), level);
+        });
         // Paper end
     }
 
@@ -302,17 +304,28 @@ public final class CraftItemStack extends ItemStack {
     public int removeEnchantment(Enchantment ench) {
         Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
 
-        // Paper start - replace entire method
-        int level = getEnchantmentLevel(ench);
-        if (level > 0) {
-            final ItemMeta itemMeta = this.getItemMeta();
-            if (itemMeta == null) return 0;
-            itemMeta.removeEnchant(ench);
-            this.setItemMeta(itemMeta);
+        // Paper start
+        if (this.handle == null) {
+            return 0;
+        }
+
+        ItemEnchantments itemEnchantments = this.handle.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY);
+        if (itemEnchantments.isEmpty()) {
+            return 0;
         }
-        // Paper end
 
-        return level;
+        Holder<net.minecraft.world.item.enchantment.Enchantment> removedEnchantment = CraftEnchantment.bukkitToMinecraftHolder(ench);
+        if (itemEnchantments.keySet().contains(removedEnchantment)) {
+            int previousLevel = itemEnchantments.getLevel(removedEnchantment);
+
+            ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(itemEnchantments); // data component api doesn't really support mutable things once already set yet
+            mutable.removeIf(enchantment -> enchantment.equals(removedEnchantment));
+            this.handle.set(DataComponents.ENCHANTMENTS, mutable.toImmutable());
+            return previousLevel;
+        }
+
+        return 0;
+        // Paper end
     }
 
     @Override
@@ -324,7 +337,13 @@ public final class CraftItemStack extends ItemStack {
 
     @Override
     public Map<Enchantment, Integer> getEnchantments() {
-        return this.hasItemMeta() ? this.getItemMeta().getEnchants() : ImmutableMap.<Enchantment, Integer>of(); // Paper - use Item Meta
+        // Paper start
+        io.papermc.paper.datacomponent.item.ItemEnchantments itemEnchantments = this.getData(io.papermc.paper.datacomponent.DataComponentTypes.ENCHANTMENTS); // empty constant might be useful here
+        if (itemEnchantments == null) {
+            return java.util.Collections.emptyMap();
+        }
+        return itemEnchantments.enchantments();
+        // Paper end
     }
 
     static Map<Enchantment, Integer> getEnchantments(net.minecraft.world.item.ItemStack item) {
@@ -526,4 +545,119 @@ public final class CraftItemStack extends ItemStack {
         return this.pdcView;
     }
     // Paper end - pdc
+    // Paper start - data component API
+    @Override
+    public <T> T getData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type) {
+        if (this.isEmpty()) {
+            return null;
+        }
+        return io.papermc.paper.datacomponent.PaperComponentType.convertDataComponentValue(this.handle.getComponents(), (io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl<T, ?>) type);
+    }
+
+    @Override
+    public boolean hasData(final io.papermc.paper.datacomponent.DataComponentType type) {
+        if (this.isEmpty()) {
+            return false;
+        }
+        return this.handle.has(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type));
+    }
+
+    @Override
+    public java.util.Set<io.papermc.paper.datacomponent.DataComponentType> getDataTypes() {
+        if (this.isEmpty()) {
+            return java.util.Collections.emptySet();
+        }
+        return io.papermc.paper.datacomponent.PaperComponentType.minecraftToBukkit(this.handle.getComponents().keySet());
+    }
+
+    @Override
+    public <T> void setData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type, final T value) {
+        Preconditions.checkArgument(value != null, "value cannot be null");
+        if (this.isEmpty()) {
+            return;
+        }
+        this.setDataInternal((io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl<T, ?>) type, value);
+    }
+
+    @Override
+    public void setData(final io.papermc.paper.datacomponent.DataComponentType.NonValued type) {
+        if (this.isEmpty()) {
+            return;
+        }
+        this.setDataInternal((io.papermc.paper.datacomponent.PaperComponentType.NonValuedImpl<?, ?>) type, null);
+    }
+
+    private <A, V> void setDataInternal(final io.papermc.paper.datacomponent.PaperComponentType<A, V> type, final A value) {
+        this.handle.set(type.getHandle(), type.getAdapter().toVanilla(value));
+    }
+
+    @Override
+    public void unsetData(final io.papermc.paper.datacomponent.DataComponentType type) {
+        if (this.isEmpty()) {
+            return;
+        }
+        this.handle.remove(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type));
+    }
+
+    @Override
+    public void resetData(final io.papermc.paper.datacomponent.DataComponentType type) {
+        if (this.isEmpty()) {
+            return;
+        }
+        this.resetData((io.papermc.paper.datacomponent.PaperComponentType<?, ?>) type);
+    }
+
+    private <M> void resetData(final io.papermc.paper.datacomponent.PaperComponentType<?, M> type) {
+        final net.minecraft.core.component.DataComponentType<M> nms = io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type);
+        final M nmsValue = this.handle.getItem().components().get(nms);
+        // if nmsValue is null, it will clear any set patch
+        // if nmsValue is not null, it will still clear any set patch because it will equal the default value
+        this.handle.set(nms, nmsValue);
+    }
+
+    @Override
+    public boolean isDataOverridden(final io.papermc.paper.datacomponent.DataComponentType type) {
+        if (this.isEmpty()) {
+            return false;
+        }
+        final net.minecraft.core.component.DataComponentType<?> nms = io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type);
+        // maybe a more efficient way is to expose the "patch" map in PatchedDataComponentMap and just check if the type exists as a key
+        return !java.util.Objects.equals(this.handle.get(nms), this.handle.getPrototype().get(nms));
+    }
+
+    @Override
+    public boolean matchesWithoutData(final ItemStack item, final java.util.Set<io.papermc.paper.datacomponent.DataComponentType> exclude, final boolean ignoreCount) {
+        // Extracted from base equals
+        final CraftItemStack craftStack = getCraftStack(item);
+        if (this.handle == craftStack.handle) return true;
+        if (this.handle == null || craftStack.handle == null) return false;
+        if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true;
+
+        net.minecraft.world.item.ItemStack left = this.handle;
+        net.minecraft.world.item.ItemStack right = craftStack.handle;
+        if (!ignoreCount && left.getCount() != right.getCount()) {
+            return false;
+        }
+        if (!left.is(right.getItem())) {
+            return false;
+        }
+
+        // It can be assumed that the prototype is equal since the type is the same. This way all we need to check is the patch
+
+        // Fast path when excluded types is empty
+        if (exclude.isEmpty()) {
+            return left.getComponentsPatch().equals(right.getComponentsPatch());
+        }
+
+        // Collect all the NMS types into a set
+        java.util.Set<net.minecraft.core.component.DataComponentType<?>> skippingTypes = new java.util.HashSet<>(exclude.size());
+        for (io.papermc.paper.datacomponent.DataComponentType api : exclude) {
+            skippingTypes.add(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(api));
+        }
+
+        // Check the patch by first stripping excluded types and then compare the trimmed patches
+        return left.getComponentsPatch().forget(skippingTypes::contains).equals(right.getComponentsPatch().forget(skippingTypes::contains));
+    }
+
+    // Paper end - data component API
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
index 1b57649d0d3db24ed32c78cf3d5ce1d9fb1353e0..674ec11b77f41905e3744c22b2ee2d828b0693ad 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
@@ -150,7 +150,7 @@ public class CraftItemType<M extends ItemMeta> implements ItemType.Typed<M>, Han
     public int getMaxStackSize() {
         // Based of the material enum air is only 0, in PerMaterialTest it is also set as special case
         // the item info itself would return 64
-        if (this == AIR) {
+        if (false && this == AIR) { // Paper - air stacks to 64
             return 0;
         }
         return this.item.components().getOrDefault(DataComponents.MAX_STACK_SIZE, 64);
@@ -270,4 +270,20 @@ public class CraftItemType<M extends ItemMeta> implements ItemType.Typed<M>, Han
         return rarity == null ? null : org.bukkit.inventory.ItemRarity.valueOf(rarity.name());
     }
     // Paper end - expand ItemRarity API
+    // Paper start - data component API
+    @Override
+    public <T> T getDefaultData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type) {
+        return io.papermc.paper.datacomponent.PaperComponentType.convertDataComponentValue(this.item.components(), ((io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl<T, ?>) type));
+    }
+
+    @Override
+    public boolean hasDefaultData(final io.papermc.paper.datacomponent.DataComponentType type) {
+        return this.item.components().has(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type));
+    }
+
+    @Override
+    public java.util.Set<io.papermc.paper.datacomponent.DataComponentType> getDefaultDataTypes() {
+        return io.papermc.paper.datacomponent.PaperComponentType.minecraftToBukkit(this.item.components().keySet());
+    }
+    // Paper end - data component API
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
index a944803771d514572f94b4e98a6d4435a009c078..82cb8cd1635c279326cec8454f1906ce35021dec 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
@@ -91,7 +91,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta {
         this.safelyAddEffects(effects, false); // Paper - limit firework effects
     }
 
-    static FireworkEffect getEffect(FireworkExplosion explosion) {
+    public static FireworkEffect getEffect(FireworkExplosion explosion) { // Paper
         FireworkEffect.Builder effect = FireworkEffect.builder()
                 .flicker(explosion.hasTwinkle())
                 .trail(explosion.hasTrail())
@@ -111,7 +111,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta {
         return effect.build();
     }
 
-    static FireworkExplosion getExplosion(FireworkEffect effect) {
+    public static FireworkExplosion getExplosion(FireworkEffect effect) { // Paper
         IntList colors = CraftMetaFirework.addColors(effect.getColors());
         IntList fadeColors = CraftMetaFirework.addColors(effect.getFadeColors());
 
diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge
new file mode 100644
index 0000000000000000000000000000000000000000..0fd276c2fdbba784c1cd95105553996b4ba2460e
--- /dev/null
+++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge
@@ -0,0 +1 @@
+io.papermc.paper.datacomponent.item.ItemComponentTypesBridgesImpl
diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge
new file mode 100644
index 0000000000000000000000000000000000000000..852ab097181491735fb9ee5ee4f70e4ceeb32e6d
--- /dev/null
+++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge
@@ -0,0 +1 @@
+io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridgeImpl
diff --git a/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d707114f53e80bf278dc640c55b515d85f03120
--- /dev/null
+++ b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java
@@ -0,0 +1,58 @@
+package io.papermc.paper.datacomponent;
+
+import com.google.common.collect.Collections2;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceLocation;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.support.RegistryHelper;
+import org.bukkit.support.environment.AllFeatures;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import java.lang.reflect.Field;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+@AllFeatures
+public class DataComponentTypesTest {
+
+    private static final Set<ResourceLocation> NOT_IN_API = Set.of(
+        ResourceLocation.parse("custom_data"),
+        ResourceLocation.parse("entity_data"),
+        ResourceLocation.parse("bees"),
+        ResourceLocation.parse("debug_stick_state"),
+        ResourceLocation.parse("block_entity_data"),
+        ResourceLocation.parse("bucket_entity_data"),
+        ResourceLocation.parse("lock"),
+        ResourceLocation.parse("creative_slot_lock")
+    );
+
+    @Test
+    public void testAllDataComponentsAreMapped() throws IllegalAccessException {
+        final Set<ResourceLocation> vanillaDataComponentTypes = new ObjectOpenHashSet<>(
+            RegistryHelper.getRegistry()
+                .lookupOrThrow(Registries.DATA_COMPONENT_TYPE)
+                .keySet()
+        );
+
+        for (final Field declaredField : DataComponentTypes.class.getDeclaredFields()) {
+            if (!DataComponentType.class.isAssignableFrom(declaredField.getType())) continue;
+
+            final DataComponentType dataComponentType = (DataComponentType) declaredField.get(null);
+            if (!vanillaDataComponentTypes.remove(CraftNamespacedKey.toMinecraft(dataComponentType.getKey()))) {
+                Assertions.fail("API defined component type " + dataComponentType.key().asMinimalString() + " is unknown to vanilla registry");
+            }
+        }
+
+        if (!vanillaDataComponentTypes.containsAll(NOT_IN_API)) {
+            Assertions.fail("API defined data components that were marked as not-yet-implemented: " + NOT_IN_API.stream().filter(Predicate.not(vanillaDataComponentTypes::contains)).map(ResourceLocation::toString).collect(Collectors.joining(", ")));
+        }
+
+        vanillaDataComponentTypes.removeAll(NOT_IN_API);
+        if (!vanillaDataComponentTypes.isEmpty()) {
+            Assertions.fail("API did not define following vanilla data component types: " + String.join(", ", Collections2.transform(vanillaDataComponentTypes, ResourceLocation::toString)));
+        }
+    }
+
+}
diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ee0491763341232844a99aa528310a3b3dca1d5
--- /dev/null
+++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java
@@ -0,0 +1,92 @@
+package io.papermc.paper.item;
+
+import io.papermc.paper.datacomponent.DataComponentTypes;
+import java.util.Set;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.support.environment.AllFeatures;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+@AllFeatures
+class ItemStackDataComponentEqualsTest {
+
+    @Test
+    public void testEqual() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 1);
+        item1.setData(DataComponentTypes.MAX_STACK_SIZE, 32);
+        item1.setData(DataComponentTypes.ITEM_NAME, Component.text("HI"));
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.setData(DataComponentTypes.MAX_STACK_SIZE, 32);
+        item2.setData(DataComponentTypes.ITEM_NAME, Component.text("HI"));
+
+        Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of()));
+    }
+
+    @Test
+    public void testEqualIgnoreComponent() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 2);
+        item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
+
+        Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE)));
+    }
+
+    @Test
+    public void testEqualIgnoreComponentAndSize() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 2);
+        item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
+
+        Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE), true));
+    }
+
+    @Test
+    public void testEqualWithoutComponent() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 1);
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
+
+        Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.WRITTEN_BOOK_CONTENT)));
+    }
+
+    @Test
+    public void testEqualRemoveComponent() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 1);
+        item1.unsetData(DataComponentTypes.MAX_STACK_SIZE);
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.unsetData(DataComponentTypes.MAX_STACK_SIZE);
+
+        Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of()));
+    }
+
+    @Test
+    public void testEqualIncludeComponentIgnoreSize() {
+        ItemStack item1 = ItemStack.of(Material.STONE, 2);
+        item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
+
+        ItemStack item2 = ItemStack.of(Material.STONE, 1);
+        item2.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
+
+        Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(), true));
+    }
+
+    @Test
+    public void testAdvancedExample() {
+        ItemStack oakLeaves = ItemStack.of(Material.OAK_LEAVES, 1);
+        oakLeaves.setData(DataComponentTypes.HIDE_TOOLTIP);
+        oakLeaves.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
+
+        ItemStack otherOakLeavesItem = ItemStack.of(Material.OAK_LEAVES, 2);
+
+        Assertions.assertTrue(oakLeaves.matchesWithoutData(otherOakLeavesItem, Set.of(DataComponentTypes.HIDE_TOOLTIP, DataComponentTypes.MAX_STACK_SIZE), true));
+    }
+}
diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..06597cc8ba23e0e1e439af5b6cbc2a7fac3c4022
--- /dev/null
+++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java
@@ -0,0 +1,416 @@
+package io.papermc.paper.item;
+
+import io.papermc.paper.datacomponent.DataComponentType;
+import io.papermc.paper.datacomponent.DataComponentTypes;
+import io.papermc.paper.datacomponent.item.ChargedProjectiles;
+import io.papermc.paper.datacomponent.item.CustomModelData;
+import io.papermc.paper.datacomponent.item.DyedItemColor;
+import io.papermc.paper.datacomponent.item.Fireworks;
+import io.papermc.paper.datacomponent.item.FoodProperties;
+import io.papermc.paper.datacomponent.item.ItemArmorTrim;
+import io.papermc.paper.datacomponent.item.ItemAttributeModifiers;
+import io.papermc.paper.datacomponent.item.ItemEnchantments;
+import io.papermc.paper.datacomponent.item.ItemLore;
+import io.papermc.paper.datacomponent.item.JukeboxPlayable;
+import io.papermc.paper.datacomponent.item.MapId;
+import io.papermc.paper.datacomponent.item.MapItemColor;
+import io.papermc.paper.datacomponent.item.PotDecorations;
+import io.papermc.paper.datacomponent.item.Tool;
+import io.papermc.paper.datacomponent.item.Unbreakable;
+import io.papermc.paper.registry.RegistryAccess;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.set.RegistrySet;
+import io.papermc.paper.registry.tag.TagKey;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.util.TriState;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.world.item.EitherHolder;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.JukeboxSongs;
+import org.bukkit.Color;
+import org.bukkit.FireworkEffect;
+import org.bukkit.JukeboxSong;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Registry;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeModifier;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.BlockType;
+import org.bukkit.block.DecoratedPot;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.EquipmentSlotGroup;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemRarity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ItemType;
+import org.bukkit.inventory.meta.ArmorMeta;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import org.bukkit.inventory.meta.CrossbowMeta;
+import org.bukkit.inventory.meta.Damageable;
+import org.bukkit.inventory.meta.FireworkMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.KnowledgeBookMeta;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+import org.bukkit.inventory.meta.MapMeta;
+import org.bukkit.inventory.meta.Repairable;
+import org.bukkit.inventory.meta.components.FoodComponent;
+import org.bukkit.inventory.meta.components.JukeboxPlayableComponent;
+import org.bukkit.inventory.meta.components.ToolComponent;
+import org.bukkit.inventory.meta.trim.ArmorTrim;
+import org.bukkit.inventory.meta.trim.TrimMaterial;
+import org.bukkit.inventory.meta.trim.TrimPattern;
+import org.bukkit.support.RegistryHelper;
+import org.bukkit.support.environment.AllFeatures;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+@AllFeatures
+class ItemStackDataComponentTest {
+
+    @Test
+    void testMaxStackSize() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_STACK_SIZE, 32, ItemMeta.class, ItemMeta::getMaxStackSize, ItemMeta::setMaxStackSize);
+    }
+
+    @Test
+    void testMaxDamage() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_DAMAGE, 120, Damageable.class, Damageable::getMaxDamage, Damageable::setMaxDamage);
+    }
+
+    @Test
+    void testDamage() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.DAMAGE, 120, Damageable.class, Damageable::getDamage, Damageable::setDamage);
+    }
+
+    @Test
+    void testUnbreakable() {
+        final ItemStack stack = new ItemStack(Material.STONE);
+        stack.setData(DataComponentTypes.UNBREAKABLE, Unbreakable.unbreakable().showInTooltip(false).build());
+
+        Assertions.assertTrue(stack.getItemMeta().isUnbreakable());
+        Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_UNBREAKABLE));
+        stack.unsetData(DataComponentTypes.UNBREAKABLE);
+        Assertions.assertFalse(stack.getItemMeta().isUnbreakable());
+    }
+
+    @Test
+    void testHideAdditionalTooltip() {
+        final ItemStack stack = new ItemStack(Material.STONE);
+        stack.setData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP);
+
+        Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP));
+        stack.unsetData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP);
+        Assertions.assertFalse(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP));
+    }
+
+    @Test
+    void testHideTooltip() {
+        ItemStack stack = new ItemStack(Material.STONE);
+        stack.setData(DataComponentTypes.HIDE_TOOLTIP);
+
+        Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP));
+        Assertions.assertTrue(stack.getItemMeta().isHideTooltip());
+        stack.unsetData(DataComponentTypes.HIDE_TOOLTIP);
+        Assertions.assertFalse(stack.getItemMeta().isHideTooltip());
+        stack = new ItemStack(Material.STONE);
+
+        stack.unsetData(DataComponentTypes.HIDE_TOOLTIP);
+        Assertions.assertFalse(stack.getItemMeta().isHideTooltip());
+        Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP));
+    }
+
+    @Test
+    void testRepairCost() {
+        final ItemStack stack = new ItemStack(Material.STONE);
+        testWithMeta(stack, DataComponentTypes.REPAIR_COST, 120, Repairable.class, Repairable::getRepairCost, Repairable::setRepairCost);
+    }
+
+    @Test
+    void testCustomName() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_NAME, Component.text("HELLO!!!!!!"), ItemMeta.class, ItemMeta::displayName, ItemMeta::displayName);
+    }
+
+    @Test
+    void testItemName() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ITEM_NAME, Component.text("HELLO!!!!!! ITEM NAME"), ItemMeta.class, ItemMeta::itemName, ItemMeta::itemName);
+    }
+
+    @Test
+    void testItemLore() {
+        List<Component> list = List.of(Component.text("1"), Component.text("2"));
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.LORE, ItemLore.lore().lines(list).build(), ItemLore::lines, ItemMeta.class, ItemMeta::lore, ItemMeta::lore);
+    }
+
+    @Test
+    void testItemRarity() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.RARITY, ItemRarity.RARE, ItemMeta.class, ItemMeta::getRarity, ItemMeta::setRarity);
+    }
+
+    @Test
+    void testItemEnchantments() {
+        final ItemStack stack = new ItemStack(Material.STONE);
+        Map<Enchantment, Integer> enchantmentIntegerMap = Map.of(Enchantment.SOUL_SPEED, 1);
+        stack.setData(DataComponentTypes.ENCHANTMENTS, ItemEnchantments.itemEnchantments(enchantmentIntegerMap, false));
+
+        Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ENCHANTS));
+        Assertions.assertEquals(1, stack.getItemMeta().getEnchantLevel(Enchantment.SOUL_SPEED));
+        Assertions.assertEquals(stack.getItemMeta().getEnchants(), enchantmentIntegerMap);
+        stack.unsetData(DataComponentTypes.ENCHANTMENTS);
+        Assertions.assertTrue(stack.getItemMeta().getEnchants().isEmpty());
+    }
+
+    @Test
+    void testItemAttributes() {
+        final ItemStack stack = new ItemStack(Material.STONE);
+        AttributeModifier modifier = new AttributeModifier(NamespacedKey.minecraft("test"), 5, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.ANY);
+        stack.setData(DataComponentTypes.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.itemAttributes().showInTooltip(false).addModifier(Attribute.ATTACK_DAMAGE, modifier).build());
+
+        Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ATTRIBUTES));
+        Assertions.assertEquals(modifier, ((List<AttributeModifier>) stack.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE)).getFirst());
+        stack.unsetData(DataComponentTypes.ATTRIBUTE_MODIFIERS);
+        Assertions.assertNull(stack.getItemMeta().getAttributeModifiers());
+    }
+
+    @Test
+    void testCustomModelData() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData(1), CustomModelData::id, ItemMeta.class, ItemMeta::getCustomModelData, ItemMeta::setCustomModelData);
+    }
+
+    @Test
+    void testEnchantmentGlintOverride() {
+        testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true, ItemMeta.class, ItemMeta::getEnchantmentGlintOverride, ItemMeta::setEnchantmentGlintOverride);
+    }
+
+    @Test
+    void testFood() {
+        FoodProperties properties = FoodProperties.food()
+            .canAlwaysEat(true)
+            .saturation(1.3F)
+            .nutrition(1)
+            .build();
+
+        final ItemStack stack = new ItemStack(Material.CROSSBOW);
+        stack.setData(DataComponentTypes.FOOD, properties);
+
+        ItemMeta meta = stack.getItemMeta();
+        FoodComponent component = meta.getFood();
+        Assertions.assertEquals(properties.canAlwaysEat(), component.canAlwaysEat());
+        Assertions.assertEquals(properties.saturation(), component.getSaturation());
+        Assertions.assertEquals(properties.nutrition(), component.getNutrition());
+
+        stack.unsetData(DataComponentTypes.FOOD);
+        meta = stack.getItemMeta();
+        Assertions.assertFalse(meta.hasFood());
+    }
+
+    @Test
+    void testTool() {
+        Tool properties = Tool.tool()
+            .damagePerBlock(1)
+            .defaultMiningSpeed(2F)
+            .addRules(List.of(
+                Tool.rule(
+                    RegistrySet.keySetFromValues(RegistryKey.BLOCK, List.of(BlockType.STONE, BlockType.GRAVEL)),
+                    2F,
+                    TriState.TRUE
+                ),
+                Tool.rule(
+                    RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK).getTag(TagKey.create(RegistryKey.BLOCK, NamespacedKey.minecraft("bamboo_blocks"))),
+                    2F,
+                    TriState.TRUE
+                )
+            ))
+            .build();
+
+        final ItemStack stack = new ItemStack(Material.CROSSBOW);
+        stack.setData(DataComponentTypes.TOOL, properties);
+
+        ItemMeta meta = stack.getItemMeta();
+        ToolComponent component = meta.getTool();
+        Assertions.assertEquals(properties.damagePerBlock(), component.getDamagePerBlock());
+        Assertions.assertEquals(properties.defaultMiningSpeed(), component.getDefaultMiningSpeed());
+
+        int idx = 0;
+        for (ToolComponent.ToolRule effect : component.getRules()) {
+            Assertions.assertEquals(properties.rules().get(idx).speed(), effect.getSpeed());
+            Assertions.assertEquals(properties.rules().get(idx).correctForDrops().toBoolean(), effect.isCorrectForDrops());
+            Assertions.assertEquals(properties.rules().get(idx).blocks().resolve(Registry.BLOCK), effect.getBlocks().stream().map(Material::asBlockType).toList());
+            idx++;
+        }
+
+        stack.unsetData(DataComponentTypes.TOOL);
+        meta = stack.getItemMeta();
+        Assertions.assertFalse(meta.hasTool());
+    }
+
+    @Test
+    void testJukeboxPlayable() {
+        JukeboxPlayable properties = JukeboxPlayable.jukeboxPlayable(JukeboxSong.MALL).build();
+
+        final ItemStack stack = new ItemStack(Material.BEEF);
+        stack.setData(DataComponentTypes.JUKEBOX_PLAYABLE, properties);
+
+        ItemMeta meta = stack.getItemMeta();
+        JukeboxPlayableComponent component = meta.getJukeboxPlayable();
+        Assertions.assertEquals(properties.jukeboxSong(), component.getSong());
+
+        stack.unsetData(DataComponentTypes.JUKEBOX_PLAYABLE);
+        meta = stack.getItemMeta();
+        Assertions.assertFalse(meta.hasJukeboxPlayable());
+    }
+
+    @Test
+    void testDyedColor() {
+        final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE);
+        Color color = Color.BLUE;
+        stack.setData(DataComponentTypes.DYED_COLOR, DyedItemColor.dyedItemColor(color, false));
+
+        Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_DYE));
+        Assertions.assertEquals(color, ((LeatherArmorMeta) stack.getItemMeta()).getColor());
+        stack.unsetData(DataComponentTypes.DYED_COLOR);
+        Assertions.assertFalse(((LeatherArmorMeta) stack.getItemMeta()).isDyed());
+    }
+
+    @Test
+    void testMapColor() {
+        testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_COLOR, MapItemColor.mapItemColor().color(Color.BLUE).build(), MapItemColor::color, MapMeta.class, MapMeta::getColor, MapMeta::setColor);
+    }
+
+    @Test
+    void testMapId() {
+        testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_ID, MapId.mapId(1), MapId::id, MapMeta.class, MapMeta::getMapId, MapMeta::setMapId);
+    }
+
+    @Test
+    void testFireworks() {
+        testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::effects, FireworkMeta.class, FireworkMeta::getEffects, (fireworkMeta, effects) -> {
+            fireworkMeta.clearEffects();
+            fireworkMeta.addEffects(effects);
+        });
+
+        testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::flightDuration, FireworkMeta.class, FireworkMeta::getPower, FireworkMeta::setPower);
+    }
+
+    @Test
+    void testTrim() {
+        final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE);
+        ItemArmorTrim armorTrim = ItemArmorTrim.itemArmorTrim(new ArmorTrim(TrimMaterial.AMETHYST, TrimPattern.BOLT), false);
+        stack.setData(DataComponentTypes.TRIM, armorTrim);
+
+        Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM));
+        Assertions.assertEquals(armorTrim.armorTrim(), ((ArmorMeta) stack.getItemMeta()).getTrim());
+        stack.unsetData(DataComponentTypes.TRIM);
+        Assertions.assertFalse(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM));
+        Assertions.assertFalse(((ArmorMeta) stack.getItemMeta()).hasTrim());
+    }
+
+    @Test
+    void testChargedProjectiles() {
+        final ItemStack stack = new ItemStack(Material.CROSSBOW);
+        ItemStack projectile = new ItemStack(Material.FIREWORK_ROCKET);
+        stack.setData(DataComponentTypes.CHARGED_PROJECTILES, ChargedProjectiles.chargedProjectiles().add(projectile).build());
+
+        CrossbowMeta meta = (CrossbowMeta) stack.getItemMeta();
+        Assertions.assertEquals(meta.getChargedProjectiles().getFirst(), projectile);
+
+        stack.unsetData(DataComponentTypes.CHARGED_PROJECTILES);
+        meta = (CrossbowMeta) stack.getItemMeta();
+        Assertions.assertTrue(meta.getChargedProjectiles().isEmpty());
+    }
+
+    @Test
+    void testPot() {
+        final ItemStack stack = new ItemStack(Material.DECORATED_POT);
+        stack.setData(DataComponentTypes.POT_DECORATIONS, PotDecorations.potDecorations().back(ItemType.DANGER_POTTERY_SHERD).build());
+
+        BlockState state = ((BlockStateMeta) stack.getItemMeta()).getBlockState();
+        DecoratedPot decoratedPot = (DecoratedPot) state;
+
+        Assertions.assertEquals(decoratedPot.getSherd(DecoratedPot.Side.BACK), Material.DANGER_POTTERY_SHERD);
+        stack.unsetData(DataComponentTypes.POT_DECORATIONS);
+        decoratedPot = (DecoratedPot) ((BlockStateMeta) stack.getItemMeta()).getBlockState();
+        Assertions.assertTrue(decoratedPot.getSherds().values().stream().allMatch((m) -> m.asItemType() == ItemType.BRICK));
+    }
+
+    @Test
+    void testRecipes() {
+        final ItemStack stack = new ItemStack(Material.KNOWLEDGE_BOOK);
+        stack.setData(DataComponentTypes.RECIPES, List.of(Key.key("paper:fun_recipe")));
+
+        final ItemMeta itemMeta = stack.getItemMeta();
+        Assertions.assertInstanceOf(KnowledgeBookMeta.class, itemMeta);
+
+        final List<NamespacedKey> recipes = ((KnowledgeBookMeta) itemMeta).getRecipes();
+        Assertions.assertEquals(recipes, List.of(new NamespacedKey("paper", "fun_recipe")));
+    }
+
+    @Test
+    void testJukeboxWithEitherKey() {
+        final ItemStack apiStack = CraftItemStack.asBukkitCopy(new net.minecraft.world.item.ItemStack(Items.MUSIC_DISC_5));
+        final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
+
+        Assertions.assertNotNull(data);
+        Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
+    }
+
+    @Test
+    void testJukeboxWithEitherHolder() {
+        final net.minecraft.world.item.ItemStack internalStack = new net.minecraft.world.item.ItemStack(Items.STONE);
+        internalStack.set(DataComponents.JUKEBOX_PLAYABLE, new net.minecraft.world.item.JukeboxPlayable(
+            new EitherHolder<>(RegistryHelper.getRegistry().lookupOrThrow(Registries.JUKEBOX_SONG).getOrThrow(JukeboxSongs.FIVE)),
+            true
+        ));
+
+        final ItemStack apiStack = CraftItemStack.asBukkitCopy(internalStack);
+        final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
+
+        Assertions.assertNotNull(data);
+        Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
+    }
+
+    private static <T, M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, final Class<M> metaType, final Function<M, T> metaGetter, final BiConsumer<M, T> metaSetter) {
+        testWithMeta(stack, type, value, Function.identity(), metaType, metaGetter, metaSetter);
+    }
+
+    private static <T, M extends ItemMeta, R> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, Function<T, R> mapper, final Class<M> metaType, final Function<M, R> metaGetter, final BiConsumer<M, R> metaSetter) {
+        ItemStack original = stack.clone();
+        stack.setData(type, value);
+
+        Assertions.assertEquals(value, stack.getData(type));
+
+        final ItemMeta meta = stack.getItemMeta();
+        final M typedMeta = Assertions.assertInstanceOf(metaType, meta);
+
+        Assertions.assertEquals(metaGetter.apply(typedMeta), mapper.apply(value));
+
+        // SETTING
+        metaSetter.accept(typedMeta, mapper.apply(value));
+        original.setItemMeta(typedMeta);
+        Assertions.assertEquals(value, original.getData(type));
+    }
+
+    private static <M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.NonValued type, final boolean value, final Class<M> metaType, final Function<M, Boolean> metaGetter, final BiConsumer<M, Boolean> metaSetter) {
+        ItemStack original = stack.clone();
+        stack.setData(type);
+
+        Assertions.assertEquals(value, stack.hasData(type));
+
+        final ItemMeta meta = stack.getItemMeta();
+        final M typedMeta = Assertions.assertInstanceOf(metaType, meta);
+
+        Assertions.assertEquals(metaGetter.apply(typedMeta), value);
+
+        // SETTING
+        metaSetter.accept(typedMeta, value);
+        original.setItemMeta(typedMeta);
+        Assertions.assertEquals(value, original.hasData(type));
+    }
+}
diff --git a/src/test/java/io/papermc/paper/item/MetaComparisonTest.java b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7cda79980729770695451adcd03b1886f60d86e3
--- /dev/null
+++ b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java
@@ -0,0 +1,281 @@
+package io.papermc.paper.item;
+
+import com.destroystokyo.paper.profile.CraftPlayerProfile;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import java.util.UUID;
+import java.util.function.Consumer;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.craftbukkit.inventory.CraftItemFactory;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemFactory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.inventory.meta.SkullMeta;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.bukkit.support.environment.AllFeatures;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+// TODO: This should technically be used to compare legacy meta vs the newly implemented
+@AllFeatures
+public class MetaComparisonTest {
+
+    private static final ItemFactory FACTORY = CraftItemFactory.instance();
+
+    @Test
+    public void testMetaApplication() {
+        ItemStack itemStack = new ItemStack(Material.STONE);
+
+        ItemMeta meta = itemStack.getItemMeta();
+        meta.setCustomModelData(1);
+
+        ItemMeta converted = FACTORY.asMetaFor(meta, Material.GOLD_INGOT);
+        Assertions.assertEquals(converted.getCustomModelData(), meta.getCustomModelData());
+
+        ItemMeta convertedAdvanced = FACTORY.asMetaFor(meta, Material.PLAYER_HEAD);
+        Assertions.assertEquals(convertedAdvanced.getCustomModelData(), meta.getCustomModelData());
+    }
+
+    @Test
+    public void testMetaApplicationDowngrading() {
+        ItemStack itemStack = new ItemStack(Material.PLAYER_HEAD);
+        PlayerProfile profile = Bukkit.createProfile("Owen1212055");
+
+        SkullMeta meta = (SkullMeta) itemStack.getItemMeta();
+        meta.setPlayerProfile(profile);
+
+        SkullMeta converted = (SkullMeta) FACTORY.asMetaFor(meta, Material.PLAYER_HEAD);
+        Assertions.assertEquals(converted.getPlayerProfile(), meta.getPlayerProfile());
+
+        SkullMeta downgraded = (SkullMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.PLAYER_HEAD);
+        Assertions.assertNull(downgraded.getPlayerProfile());
+    }
+
+    @Test
+    public void testMetaApplicationDowngradingPotion() {
+        ItemStack itemStack = new ItemStack(Material.POTION);
+        Color color = Color.BLUE;
+
+        PotionMeta meta = (PotionMeta) itemStack.getItemMeta();
+        meta.setColor(color);
+
+        PotionMeta converted = (PotionMeta) FACTORY.asMetaFor(meta, Material.POTION);
+        Assertions.assertEquals(converted.getColor(), color);
+
+        PotionMeta downgraded = (PotionMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.POTION);
+        Assertions.assertNull(downgraded.getColor());
+    }
+
+    @Test
+    public void testNullMeta() {
+        ItemStack itemStack = new ItemStack(Material.AIR);
+
+        Assertions.assertFalse(itemStack.hasItemMeta());
+        Assertions.assertNull(itemStack.getItemMeta());
+    }
+
+    @Test
+    public void testPotionMeta() {
+        PotionEffect potionEffect = new PotionEffect(PotionEffectType.SPEED, 10, 10, false);
+        ItemStack nmsItemStack = new ItemStack(Material.POTION, 1);
+
+        testSetAndGet(nmsItemStack,
+            (meta) -> ((PotionMeta) meta).addCustomEffect(potionEffect, true),
+            (meta) -> Assertions.assertEquals(potionEffect, ((PotionMeta) meta).getCustomEffects().getFirst())
+        );
+    }
+
+    @Test
+    public void testEnchantment() {
+        ItemStack stack = new ItemStack(Material.STICK, 1);
+
+        testSetAndGet(stack,
+            (meta) -> Assertions.assertTrue(meta.addEnchant(Enchantment.SHARPNESS, 1, true)),
+            (meta) -> Assertions.assertEquals(1, meta.getEnchantLevel(Enchantment.SHARPNESS))
+        );
+    }
+
+    @Test
+    @Disabled
+    public void testPlayerHead() {
+        PlayerProfile profile = new CraftPlayerProfile(UUID.randomUUID(), "Owen1212055");
+        ItemStack stack = new ItemStack(Material.PLAYER_HEAD, 1);
+
+        testSetAndGet(stack,
+            (meta) -> ((SkullMeta) meta).setPlayerProfile(profile),
+            (meta) -> {
+                Assertions.assertTrue(((SkullMeta) meta).hasOwner());
+                Assertions.assertEquals(profile, ((SkullMeta) meta).getPlayerProfile());
+            }
+        );
+
+        testSetAndGet(stack,
+            (meta) -> ((SkullMeta) meta).setOwner("Owen1212055"),
+            (meta) -> {
+                Assertions.assertTrue(((SkullMeta) meta).hasOwner());
+                Assertions.assertEquals("Owen1212055", ((SkullMeta) meta).getOwner());
+            }
+        );
+    }
+
+    @Test
+    public void testBookMetaAuthor() {
+        ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
+
+        // Legacy string
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).setAuthor("Owen1212055"),
+            (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getAuthor())
+        );
+
+        // Component Colored
+        Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).author(coloredName),
+            (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).author())
+        );
+
+        // Simple text
+        Component name = Component.text("Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).author(name),
+            (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).author())
+        );
+    }
+
+    @Test
+    public void testBookMetaTitle() {
+        ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
+
+        // Legacy string
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).setTitle("Owen1212055"),
+            (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getTitle())
+        );
+
+        // Component Colored
+        Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).title(coloredName),
+            (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).title())
+        );
+
+        // Simple text
+        Component name = Component.text("Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).title(name),
+            (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).title())
+        );
+    }
+
+
+    @Test
+    public void testWriteableBookPages() {
+        ItemStack stack = new ItemStack(Material.WRITABLE_BOOK, 1);
+
+        // Writeable books are serialized as plain text, but has weird legacy color support.
+        // So, we need to test to make sure that all works here.
+
+        // Legacy string
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPage("Owen1212055"),
+            (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1))
+        );
+
+        // Legacy string colored
+        String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPage(translatedLegacy),
+            (meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1))
+        );
+
+        // Component Colored
+        Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(coloredName),
+            (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1))
+        );
+
+        // Simple text
+        Component name = Component.text("Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(name),
+            (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
+        );
+
+        // Simple text + hover... should NOT be saved
+        // As this is plain text
+        Component nameWithHover = Component.text("Owen1212055")
+            .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover")));
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(nameWithHover),
+            (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
+        );
+    }
+
+    @Test
+    public void testWrittenBookPages() {
+        ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
+
+        // Writeable books are serialized as plain text, but has weird legacy color support.
+        // So, we need to test to make sure that all works here.
+
+        // Legacy string
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPage("Owen1212055"),
+            (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1))
+        );
+
+        // Legacy string colored
+        String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPage(translatedLegacy),
+            (meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1))
+        );
+
+        // Component Colored
+        Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(coloredName),
+            (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1))
+        );
+
+        // Simple text
+        Component name = Component.text("Owen1212055");
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(name),
+            (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
+        );
+
+        // Simple text + hover... should be saved
+        Component nameWithHover = Component.text("Owen1212055")
+            .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover")));
+        testSetAndGet(stack,
+            (meta) -> ((BookMeta) meta).addPages(nameWithHover),
+            (meta) -> Assertions.assertEquals(nameWithHover, ((BookMeta) meta).page(1))
+        );
+    }
+
+    private void testSetAndGet(ItemStack itemStack, Consumer<ItemMeta> set, Consumer<ItemMeta> get) {
+        ItemMeta craftMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); // TODO: This should be converted to use the old meta when this is added.
+        ItemMeta paperMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack));
+        // Test craft meta
+        set.accept(craftMeta);
+        get.accept(craftMeta);
+
+        // Test paper meta
+        set.accept(paperMeta);
+        get.accept(paperMeta);
+    }
+}
diff --git a/src/test/java/org/bukkit/PerMaterialTest.java b/src/test/java/org/bukkit/PerMaterialTest.java
index 629fccec144b5d66addc0e8258cde90e81904e1c..6961730365da9083e8963200ecc5f85dbc654f35 100644
--- a/src/test/java/org/bukkit/PerMaterialTest.java
+++ b/src/test/java/org/bukkit/PerMaterialTest.java
@@ -101,17 +101,13 @@ public class PerMaterialTest {
 
         final ItemStack bukkit = new ItemStack(material);
         final CraftItemStack craft = CraftItemStack.asCraftCopy(bukkit);
-        if (material == Material.AIR) {
-            final int MAX_AIR_STACK = 0 /* Why can't I hold all of these AIR? */;
-            assertThat(material.getMaxStackSize(), is(MAX_AIR_STACK));
-            assertThat(bukkit.getMaxStackSize(), is(MAX_AIR_STACK));
-            assertThat(craft.getMaxStackSize(), is(MAX_AIR_STACK));
-        } else {
+
+        // Paper - remove air exception
             int max = CraftMagicNumbers.getItem(material).components().getOrDefault(DataComponents.MAX_STACK_SIZE, 64);
             assertThat(material.getMaxStackSize(), is(max));
             assertThat(bukkit.getMaxStackSize(), is(max));
             assertThat(craft.getMaxStackSize(), is(max));
-        }
+        // Paper - remove air exception
     }
 
     @ParameterizedTest
diff --git a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
index b717a5ffa567781b0687bbe238b62844214db284..2d60c06b70201e4c993498af3c8e52da94b5a63e 100644
--- a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
+++ b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
@@ -100,6 +100,7 @@ public class RegistriesArgumentProvider implements ArgumentsProvider {
         register(RegistryKey.MAP_DECORATION_TYPE, MapCursor.Type.class, Registries.MAP_DECORATION_TYPE, CraftMapCursor.CraftType.class, MapDecorationType.class);
         register(RegistryKey.BANNER_PATTERN, PatternType.class, Registries.BANNER_PATTERN, CraftPatternType.class, BannerPattern.class);
         register(RegistryKey.MENU, MenuType.class, Registries.MENU, CraftMenuType.class, net.minecraft.world.inventory.MenuType.class);
+        register(RegistryKey.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.DataComponentType.class, Registries.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.PaperComponentType.class, net.minecraft.core.component.DataComponentType.class);
     }
 
     private static void register(RegistryKey registryKey, Class bukkit, ResourceKey registry, Class craft, Class minecraft) { // Paper