diff --git a/packages/core/src/components/navigation-menu/index.ts b/packages/core/src/components/navigation-menu/index.ts
new file mode 100644
index 0000000..0363e9a
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/index.ts
@@ -0,0 +1,16 @@
+import { NavigationMenuRoot } from "./navigation-menu-root";
+import { NavigationMenuList } from "./navigation-menu-list";
+import { NavigationMenuItem } from "./navigation-menu-item";
+import { NavigationMenuTrigger } from "./navigation-menu-trigger";
+import { NavigationMenuContent } from "./navigation-menu-content";
+import { NavigationMenuLink } from "./navigation-menu-link";
+import { NavigationMenuViewport } from "./navigation-menu-viewport";
+export const NavigationMenu = Object.assign(NavigationMenuRoot, { List: NavigationMenuList, Item: NavigationMenuItem, Trigger: NavigationMenuTrigger, Content: NavigationMenuContent, Link: NavigationMenuLink, Viewport: NavigationMenuViewport });
+export type { NavigationMenuRootProps } from "./navigation-menu.props";
+export type { NavigationMenuListProps } from "./navigation-menu.props";
+export type { NavigationMenuItemProps } from "./navigation-menu.props";
+export type { NavigationMenuTriggerProps } from "./navigation-menu.props";
+export type { NavigationMenuContentProps } from "./navigation-menu.props";
+export type { NavigationMenuLinkProps } from "./navigation-menu.props";
+export type { NavigationMenuViewportProps } from "./navigation-menu.props";
+export { NavigationMenuRootPropsSchema, NavigationMenuMeta } from "./navigation-menu.props";
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-content.tsx b/packages/core/src/components/navigation-menu/navigation-menu-content.tsx
new file mode 100644
index 0000000..6f313a3
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-content.tsx
@@ -0,0 +1,26 @@
+import type { JSX } from "solid-js";
+import { Show, splitProps } from "solid-js";
+import { useNavigationMenuItemContext } from "./navigation-menu-context";
+import type { NavigationMenuContentProps } from "./navigation-menu.props";
+
+/**
+ * Dropdown content panel for a NavigationMenu item. Renders only when the
+ * parent item is active, unless forceMount is true.
+ */
+export function NavigationMenuContent(props: NavigationMenuContentProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["forceMount", "children"]);
+ const itemCtx = useNavigationMenuItemContext();
+
+ return (
+
+
+ {local.children}
+
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-context.ts b/packages/core/src/components/navigation-menu/navigation-menu-context.ts
new file mode 100644
index 0000000..d6f5599
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-context.ts
@@ -0,0 +1,37 @@
+import { createContext, useContext } from "solid-js";
+import type { Accessor } from "solid-js";
+
+interface NavigationMenuContextValue {
+ value: Accessor;
+ setValue: (value: string) => void;
+ orientation: Accessor<"horizontal" | "vertical">;
+ scheduleOpen: (itemValue: string) => void;
+ cancelAndClose: () => void;
+}
+
+const NavigationMenuContext = createContext();
+
+/** Returns the NavigationMenu root context. Throws if used outside . */
+export function useNavigationMenuContext(): NavigationMenuContextValue {
+ const ctx = useContext(NavigationMenuContext);
+ if (!ctx) throw new Error("[PettyUI] NavigationMenu parts must be used within .");
+ return ctx;
+}
+
+export { NavigationMenuContext };
+
+interface NavigationMenuItemContextValue {
+ value: string;
+ isActive: Accessor;
+}
+
+const NavigationMenuItemContext = createContext();
+
+/** Returns the NavigationMenu item context. Throws if used outside . */
+export function useNavigationMenuItemContext(): NavigationMenuItemContextValue {
+ const ctx = useContext(NavigationMenuItemContext);
+ if (!ctx) throw new Error("[PettyUI] NavigationMenu.Trigger/Content must be inside .");
+ return ctx;
+}
+
+export { NavigationMenuItemContext };
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-item.tsx b/packages/core/src/components/navigation-menu/navigation-menu-item.tsx
new file mode 100644
index 0000000..6b51286
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-item.tsx
@@ -0,0 +1,31 @@
+import type { JSX } from "solid-js";
+import { createUniqueId, splitProps } from "solid-js";
+import { NavigationMenuItemContext, useNavigationMenuContext } from "./navigation-menu-context";
+import type { NavigationMenuItemProps } from "./navigation-menu.props";
+
+/**
+ * A single item within the NavigationMenu list. Provides per-item context
+ * (value and active state) to Trigger and Content children.
+ */
+export function NavigationMenuItem(props: NavigationMenuItemProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["value", "children"]);
+ const ctx = useNavigationMenuContext();
+
+ const itemValue = local.value ?? createUniqueId();
+ const isActive = () => ctx.value() === itemValue;
+
+ const itemCtx = { value: itemValue, isActive };
+
+ return (
+
+
+ {local.children}
+
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-link.tsx b/packages/core/src/components/navigation-menu/navigation-menu-link.tsx
new file mode 100644
index 0000000..4be09fc
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-link.tsx
@@ -0,0 +1,23 @@
+import type { JSX } from "solid-js";
+import { splitProps } from "solid-js";
+import type { NavigationMenuLinkProps } from "./navigation-menu.props";
+
+/**
+ * An anchor link for use inside NavigationMenu. Marks itself active via
+ * data-active when the active prop is true.
+ */
+export function NavigationMenuLink(props: NavigationMenuLinkProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["active", "children"]);
+
+ return (
+
+ {local.children}
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-list.tsx b/packages/core/src/components/navigation-menu/navigation-menu-list.tsx
new file mode 100644
index 0000000..18d2ed2
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-list.tsx
@@ -0,0 +1,25 @@
+import type { JSX } from "solid-js";
+import { splitProps } from "solid-js";
+import { useNavigationMenuContext } from "./navigation-menu-context";
+import type { NavigationMenuListProps } from "./navigation-menu.props";
+
+/**
+ * Ordered list container for NavigationMenu items. Renders as a
with
+ * role="menubar" and the correct orientation data attribute.
+ */
+export function NavigationMenuList(props: NavigationMenuListProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["children"]);
+ const ctx = useNavigationMenuContext();
+
+ return (
+
+ {local.children}
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-root.tsx b/packages/core/src/components/navigation-menu/navigation-menu-root.tsx
new file mode 100644
index 0000000..28d34b2
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-root.tsx
@@ -0,0 +1,75 @@
+import type { JSX } from "solid-js";
+import { onCleanup, splitProps } from "solid-js";
+import { createControllableSignal } from "../../primitives/create-controllable-signal";
+import { NavigationMenuContext } from "./navigation-menu-context";
+import type { NavigationMenuRootProps } from "./navigation-menu.props";
+
+/**
+ * Root container for NavigationMenu. Manages which item is active and
+ * provides context to all sub-components.
+ */
+export function NavigationMenuRoot(props: NavigationMenuRootProps): JSX.Element {
+ const [local, rest] = splitProps(props, [
+ "value",
+ "defaultValue",
+ "onValueChange",
+ "orientation",
+ "delayDuration",
+ "children",
+ ]);
+
+ const [value, setValue] = createControllableSignal({
+ value: () => local.value,
+ defaultValue: () => local.defaultValue ?? "",
+ onChange: (v) => local.onValueChange?.(v),
+ });
+
+ let openTimer: ReturnType | undefined;
+ const delay = () => local.delayDuration ?? 200;
+
+ /** Schedule opening a menu item after the hover delay. */
+ const scheduleOpen = (itemValue: string) => {
+ if (openTimer !== undefined) {
+ clearTimeout(openTimer);
+ openTimer = undefined;
+ }
+ openTimer = setTimeout(() => {
+ setValue(itemValue);
+ openTimer = undefined;
+ }, delay());
+ };
+
+ /** Cancel any pending open timer and close the active item. */
+ const cancelAndClose = () => {
+ if (openTimer !== undefined) {
+ clearTimeout(openTimer);
+ openTimer = undefined;
+ }
+ setValue("");
+ };
+
+ onCleanup(() => {
+ if (openTimer !== undefined) clearTimeout(openTimer);
+ });
+
+ const ctx = {
+ value: value as () => string,
+ setValue,
+ orientation: () => local.orientation ?? "horizontal",
+ scheduleOpen,
+ cancelAndClose,
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-trigger.tsx b/packages/core/src/components/navigation-menu/navigation-menu-trigger.tsx
new file mode 100644
index 0000000..6672ac4
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-trigger.tsx
@@ -0,0 +1,53 @@
+import type { JSX } from "solid-js";
+import { splitProps } from "solid-js";
+import { useNavigationMenuContext, useNavigationMenuItemContext } from "./navigation-menu-context";
+import type { NavigationMenuTriggerProps } from "./navigation-menu.props";
+
+/**
+ * Button that opens the associated NavigationMenu.Content on click or hover.
+ * Manages hover-intent via the root context's scheduleOpen/cancelAndClose.
+ */
+export function NavigationMenuTrigger(props: NavigationMenuTriggerProps): JSX.Element {
+ const [local, rest] = splitProps(props, [
+ "children",
+ "onClick",
+ "onPointerEnter",
+ "onPointerLeave",
+ ]);
+
+ const ctx = useNavigationMenuContext();
+ const itemCtx = useNavigationMenuItemContext();
+
+ const handleClick: JSX.EventHandler = (e) => {
+ if (typeof local.onClick === "function") local.onClick(e);
+ const next = ctx.value() === itemCtx.value ? "" : itemCtx.value;
+ ctx.setValue(next);
+ };
+
+ const handlePointerEnter: JSX.EventHandler = (e) => {
+ if (typeof local.onPointerEnter === "function") local.onPointerEnter(e);
+ ctx.scheduleOpen(itemCtx.value);
+ };
+
+ const handlePointerLeave: JSX.EventHandler = (e) => {
+ if (typeof local.onPointerLeave === "function") local.onPointerLeave(e);
+ ctx.cancelAndClose();
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu-viewport.tsx b/packages/core/src/components/navigation-menu/navigation-menu-viewport.tsx
new file mode 100644
index 0000000..39b6174
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu-viewport.tsx
@@ -0,0 +1,24 @@
+import type { JSX } from "solid-js";
+import { splitProps } from "solid-js";
+import { useNavigationMenuContext } from "./navigation-menu-context";
+import type { NavigationMenuViewportProps } from "./navigation-menu.props";
+
+/**
+ * Viewport container that can host active NavigationMenu content outside
+ * the list DOM structure, enabling animated panel transitions.
+ */
+export function NavigationMenuViewport(props: NavigationMenuViewportProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["forceMount"]);
+ const ctx = useNavigationMenuContext();
+ const isOpen = () => ctx.value() !== "";
+
+ return (
+
+ );
+}
diff --git a/packages/core/src/components/navigation-menu/navigation-menu.props.ts b/packages/core/src/components/navigation-menu/navigation-menu.props.ts
new file mode 100644
index 0000000..defae48
--- /dev/null
+++ b/packages/core/src/components/navigation-menu/navigation-menu.props.ts
@@ -0,0 +1,28 @@
+import { z } from "zod/v4";
+import type { JSX } from "solid-js";
+import type { ComponentMeta } from "../../meta";
+
+export const NavigationMenuRootPropsSchema = z.object({
+ value: z.string().optional().describe("Controlled active item value (the currently open dropdown)"),
+ defaultValue: z.string().optional().describe("Initial active item (uncontrolled)"),
+ orientation: z.enum(["horizontal", "vertical"]).optional().describe("Menu orientation. Defaults to 'horizontal'"),
+ delayDuration: z.number().optional().describe("Delay in ms before dropdown opens on hover. Defaults to 200"),
+});
+
+export interface NavigationMenuRootProps extends z.infer, Omit, keyof z.infer> {
+ onValueChange?: (value: string) => void;
+ children: JSX.Element;
+}
+export interface NavigationMenuListProps extends JSX.HTMLAttributes { children: JSX.Element; }
+export interface NavigationMenuItemProps extends JSX.HTMLAttributes { value?: string; children: JSX.Element; }
+export interface NavigationMenuTriggerProps extends JSX.ButtonHTMLAttributes { children?: JSX.Element; }
+export interface NavigationMenuContentProps extends JSX.HTMLAttributes { forceMount?: boolean; children?: JSX.Element; }
+export interface NavigationMenuLinkProps extends JSX.AnchorHTMLAttributes { active?: boolean; children?: JSX.Element; }
+export interface NavigationMenuViewportProps extends JSX.HTMLAttributes { forceMount?: boolean; }
+
+export const NavigationMenuMeta: ComponentMeta = {
+ name: "NavigationMenu",
+ description: "Horizontal navigation bar with dropdown submenus, hover intent, and keyboard support",
+ parts: ["Root", "List", "Item", "Trigger", "Content", "Link", "Viewport", "Indicator"] as const,
+ requiredParts: ["Root", "List", "Item"] as const,
+} as const;
diff --git a/packages/core/tests/components/navigation-menu/navigation-menu.test.tsx b/packages/core/tests/components/navigation-menu/navigation-menu.test.tsx
new file mode 100644
index 0000000..6346f6a
--- /dev/null
+++ b/packages/core/tests/components/navigation-menu/navigation-menu.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen } from "@solidjs/testing-library";
+import { describe, expect, it } from "vitest";
+import { NavigationMenu } from "../../../src/components/navigation-menu/index";
+import { NavigationMenuRootPropsSchema, NavigationMenuMeta } from "../../../src/components/navigation-menu/navigation-menu.props";
+
+describe("NavigationMenu", () => {
+ it("renders menu with link items", () => {
+ render(() => (
+
+
+
+ About
+
+
+
+ ));
+ expect(screen.getByText("About")).toBeTruthy();
+ });
+
+ it("renders trigger with content", () => {
+ render(() => (
+
+
+
+ Products
+