diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerRecipeBookClickEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerRecipeBookClickEvent.java
new file mode 100644
index 0000000000..eb8623184f
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerRecipeBookClickEvent.java
@@ -0,0 +1,101 @@
+package org.bukkit.event.player;
+
+import com.google.common.base.Preconditions;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.bukkit.inventory.CraftingRecipe;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a player clicks a recipe in the recipe book.
+ */
+public class PlayerRecipeBookClickEvent extends PlayerEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+ private final Recipe originalRecipe;
+ private Recipe recipe;
+ private boolean shiftClick;
+
+ public PlayerRecipeBookClickEvent(@NotNull final Player player, @NotNull final Recipe recipe, boolean shiftClick) {
+ super(player);
+ this.originalRecipe = recipe;
+ this.recipe = recipe;
+ this.shiftClick = shiftClick;
+ }
+
+ /**
+ * Gets the original recipe the player was trying to craft.
+ * This will not reflect any changes made with {@link setRecipe}.
+ *
+ * @return the original recipe
+ */
+ @NotNull
+ public Recipe getOriginalRecipe() {
+ return this.originalRecipe;
+ }
+
+ /**
+ * Gets the recipe the player is trying to craft.
+ * This will reflect changes made with {@link setRecipe}.
+ *
+ * @return the recipe
+ */
+ @NotNull
+ public Recipe getRecipe() {
+ return this.recipe;
+ }
+
+ /**
+ * Set the recipe that will be used.
+ * The game will attempt to move the ingredients for this recipe into the
+ * appropriate slots.
+ *
+ * If the original recipe is a {@link CraftingRecipe} the provided recipe
+ * must also be a {@link CraftingRecipe}, otherwise the provided recipe must
+ * be of the same type as the original recipe.
+ *
+ * @param recipe the recipe to be used
+ */
+ public void setRecipe(@NotNull Recipe recipe) {
+ Preconditions.checkArgument(recipe != null, "recipe cannot be null");
+ if (this.originalRecipe instanceof CraftingRecipe) { // Any type of crafting recipe is acceptable
+ Preconditions.checkArgument(recipe instanceof CraftingRecipe, "provided recipe must be a crafting recipe");
+ } else { // Other recipes must be the same type
+ Preconditions.checkArgument(this.originalRecipe.getClass() == recipe.getClass(), "provided recipe must be of the same type as original recipe");
+ }
+ this.recipe = recipe;
+ }
+
+ /**
+ * If true the game will attempt to move the ingredients for as many copies
+ * of this recipe as possible into the appropriate slots, otherwise only 1
+ * copy will be moved.
+ *
+ * @return whether as many copies as possible should be moved
+ */
+ public boolean isShiftClick() {
+ return this.shiftClick;
+ }
+
+ /**
+ * Sets if the game will attempt to move the ingredients for as many copies
+ * of this recipe as possible into the appropriate slots.
+ *
+ * @param shiftClick whether as many copies as possible should be moved
+ */
+ public void setShiftClick(boolean shiftClick) {
+ this.shiftClick = shiftClick;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ @NotNull
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java
new file mode 100644
index 0000000000..e4bf772f7e
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java
@@ -0,0 +1,89 @@
+package org.bukkit.inventory;
+
+import com.google.common.base.Preconditions;
+import org.bukkit.Keyed;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.recipe.CraftingBookCategory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents a shaped or shapeless crafting recipe.
+ */
+public abstract class CraftingRecipe implements Recipe, Keyed {
+ private final NamespacedKey key;
+ private final ItemStack output;
+ private String group = "";
+ private CraftingBookCategory category = CraftingBookCategory.MISC;
+
+ protected CraftingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result) {
+ Preconditions.checkArgument(key != null, "key cannot be null");
+ Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result.");
+ this.key = key;
+ this.output = new ItemStack(result);
+ }
+
+ @NotNull
+ @Override
+ public NamespacedKey getKey() {
+ return key;
+ }
+
+ /**
+ * Get the result of this recipe.
+ *
+ * @return The result stack.
+ */
+ @Override
+ @NotNull
+ public ItemStack getResult() {
+ return output.clone();
+ }
+
+ /**
+ * Get the group of this recipe. Recipes with the same group may be grouped
+ * together when displayed in the client.
+ *
+ * @return recipe group. An empty string denotes no group. May not be null.
+ */
+ @NotNull
+ public String getGroup() {
+ return group;
+ }
+
+ /**
+ * Set the group of this recipe. Recipes with the same group may be grouped
+ * together when displayed in the client.
+ *
+ * @param group recipe group. An empty string denotes no group. May not be
+ * null.
+ */
+ public void setGroup(@NotNull String group) {
+ Preconditions.checkArgument(group != null, "group cannot be null");
+ this.group = group;
+ }
+
+ /**
+ * Gets the category which this recipe will appear in the recipe book under.
+ *
+ * Defaults to {@link CraftingBookCategory#MISC} if not set.
+ *
+ * @return recipe book category
+ */
+ @NotNull
+ public CraftingBookCategory getCategory() {
+ return category;
+ }
+
+ /**
+ * Sets the category which this recipe will appear in the recipe book under.
+ *
+ * Defaults to {@link CraftingBookCategory#MISC} if not set.
+ *
+ * @param category recipe book category
+ */
+ public void setCategory(@NotNull CraftingBookCategory category) {
+ Preconditions.checkArgument(category != null, "category cannot be null");
+ this.category = category;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java
index 8971ec20d3..05f1acaac3 100644
--- a/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java
+++ b/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java
@@ -4,23 +4,17 @@ import com.google.common.base.Preconditions;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import org.bukkit.Keyed;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
-import org.bukkit.inventory.recipe.CraftingBookCategory;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
/**
* Represents a shaped (ie normal) crafting recipe.
*/
-public class ShapedRecipe implements Recipe, Keyed {
- private final NamespacedKey key;
- private final ItemStack output;
+public class ShapedRecipe extends CraftingRecipe {
private String[] rows;
private Map