diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx b/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx
new file mode 100644
index 00000000..5f64d9a7
--- /dev/null
+++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx
@@ -0,0 +1,53 @@
+import React from "react"
+
+import { createIconRender } from "components/Icons"
+
+import "./index.less"
+
+export default (props) => {
+ const { items = [], cords, clickedComponent } = props
+
+ const handleItemClick = async (item) => {
+ if (typeof item.action === "function") {
+ await item.action(clickedComponent)
+ }
+ }
+
+ const renderItems = () => {
+ if (items.length === 0) {
+ return
+ }
+
+ return items.map((item, index) => {
+ if (item.type === "separator") {
+ return
+ }
+
+ return handleItemClick(item)}
+ className="item"
+ >
+
+ {item.label}
+
+ {item.description &&
+ {item.description}
+
}
+ {createIconRender(item.icon)}
+
+ })
+ }
+
+ return
+ {renderItems()}
+
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.less b/packages/app/src/cores/contextMenu/components/contextMenu/index.less
new file mode 100644
index 00000000..21607e0f
--- /dev/null
+++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.less
@@ -0,0 +1,80 @@
+.contextMenu {
+ position: absolute;
+ z-index: 100000;
+
+ display: flex;
+ flex-direction: column;
+
+ top: 0;
+ left: 0;
+
+ width: 200px;
+ height: fit-content;
+
+ background-color: var(--background-color-primary);
+ border: 1px solid var(--border-color);
+
+ border-radius: 8px;
+
+ padding: 10px;
+
+ font-family: "Recursive", sans-serif;
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: var(--text-color);
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ p,
+ span {
+ margin: 0;
+ word-break: break-all;
+ }
+
+ .item {
+ position: relative;
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+
+ width: 100%;
+
+ padding: 5px 10px 5px 20px;
+
+ transition: all 50ms ease-in-out;
+
+ border-radius: 8px;
+
+ overflow: hidden;
+
+ &:hover {
+ background-color: var(--background-color-accent);
+ padding-left: 25px;
+ }
+
+ &:active {
+ background-color: var(--background-color-primary2);
+ transform: scale(0.95);
+ }
+ }
+
+ .context-menu-separator {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: 1px;
+
+ margin: 10px 0;
+
+ background-color: var(--border-color);
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/contextMenu/index.js b/packages/app/src/cores/contextMenu/index.js
new file mode 100644
index 00000000..dcb336d9
--- /dev/null
+++ b/packages/app/src/cores/contextMenu/index.js
@@ -0,0 +1,124 @@
+import React from "react"
+import Core from "evite/src/core"
+
+import { DOMWindow } from "components/RenderWindow"
+import ContextMenu from "./components/contextMenu"
+
+import Contexts from "schemas/menu-contexts"
+
+export default class ContextMenuCore extends Core {
+ static namespace = "ContextMenu"
+ static public = ["show", "hide", "registerContext"]
+
+ contexts = Object()
+ defaultContext = [
+ {
+ label: "Report a bug",
+ icon: "AlertTriangle",
+ action: (parent, element) => {
+ app.eventBus.emit("app.reportBug", {
+ parent,
+ element
+ })
+ }
+ }
+ ]
+
+ DOMWindow = new DOMWindow({
+ id: "contextMenu",
+ className: "contextMenuWrapper",
+ clickOutsideToClose: true,
+ })
+
+ async initialize() {
+ document.addEventListener("contextmenu", this.handleEvent)
+ }
+
+ registerContext = (element, context) => {
+ this.contexts[element] = context
+ }
+
+ generateItems = (element) => {
+ let items = []
+
+ // find the closest context with attribute (context-menu)
+ // if not found, use default context
+ const parentElement = element.closest("[context-menu]")
+
+ if (parentElement) {
+ const context = parentElement.getAttribute("context-menu")
+
+ // if context is not registered, try to fetch it from the constants contexts object
+ if (!this.contexts[context]) {
+ items = Contexts[context] || []
+ } else {
+ items = this.contexts[context] ?? []
+ }
+
+ if (typeof items === "function") {
+ items = items(
+ parentElement,
+ element,
+ {
+ close: this.hide
+ }
+ )
+ }
+ }
+
+ // fullfill each item with a correspondent index if missing declared
+ items = items.map((item, index) => {
+ if (!item.index) {
+ item.index = index
+ }
+
+ return item
+ })
+
+ // short items (if has declared index)
+ items = items.sort((a, b) => a.index - b.index)
+
+ if (items.length > 0) {
+ items.push({
+ type: "separator"
+ })
+ }
+
+ items.push(...this.defaultContext)
+
+ return items
+ }
+
+ handleEvent = (event) => {
+ event.preventDefault()
+
+ // get the cords of the mouse
+ const x = event.clientX
+ const y = event.clientY
+
+ // get the component that was clicked
+ const component = document.elementFromPoint(x, y)
+
+ // check if is clicking inside a context menu or a children inside a context menu
+ if (component.classList.contains("contextMenu") || component.closest(".contextMenu")) {
+ return
+ }
+
+ this.show({
+ cords: {
+ x,
+ y,
+ },
+ clickedComponent: component,
+ items: this.generateItems(component),
+ })
+ }
+
+ show = (payload) => {
+ this.DOMWindow.render(React.createElement(ContextMenu, payload))
+ }
+
+ hide = () => {
+ this.DOMWindow.remove()
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/index.js b/packages/app/src/cores/index.js
index eb90b750..493b78be 100644
--- a/packages/app/src/cores/index.js
+++ b/packages/app/src/cores/index.js
@@ -3,6 +3,7 @@ import APICore from "./api"
import StyleCore from "./style"
import PermissionsCore from "./permissions"
import SearchCore from "./search"
+import ContextMenuCore from "./contextMenu"
import I18nCore from "./i18n"
import NotificationsCore from "./notifications"
@@ -24,4 +25,5 @@ export default [
ShortcutsCore,
AudioPlayer,
+ ContextMenuCore,
]
\ No newline at end of file