react-floating-dock 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lemon Oluwatobi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # react-floating-dock
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A floating, fully-rounded bottom navigation bar for React, in the style of WhatsApp's mobile tab
4
+ bar: an animated pill slides behind the active tab, and an optional "More" button opens the rest
5
+ of your destinations in a bottom sheet.
6
+
7
+ Framework-agnostic — it doesn't assume Next.js, React Router, or Tailwind. Bring your own router's
8
+ `Link` component and your own colors.
9
+
10
+ ![Example](./example/screenshot.png)
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install react-floating-dock framer-motion
16
+ ```
17
+
18
+ `react`, `react-dom` and `framer-motion` are peer dependencies — install them if your project
19
+ doesn't already have them.
20
+
21
+ ## Quick start
22
+
23
+ ```tsx
24
+ import { FloatingDock, type DockItem } from "react-floating-dock";
25
+ import "react-floating-dock/style.css";
26
+ import { Home, Wallet, ArrowLeftRight, User, Settings } from "lucide-react";
27
+ import Link from "next/link"; // or your router's Link
28
+ import { usePathname } from "next/navigation";
29
+
30
+ const items: DockItem[] = [
31
+ { key: "home", href: "/", label: "Home", icon: Home },
32
+ { key: "wallet", href: "/wallet", label: "Wallet", icon: Wallet },
33
+ { key: "activity", href: "/activity", label: "Activity", icon: ArrowLeftRight },
34
+ ];
35
+
36
+ const moreItems: DockItem[] = [
37
+ { key: "profile", href: "/profile", label: "Profile", icon: User },
38
+ { key: "settings", href: "/settings", label: "Settings", icon: Settings },
39
+ ];
40
+
41
+ export function AppNav() {
42
+ const pathname = usePathname();
43
+
44
+ return (
45
+ <FloatingDock
46
+ items={items}
47
+ moreItems={moreItems}
48
+ isActive={(href) => pathname === href}
49
+ LinkComponent={Link}
50
+ />
51
+ );
52
+ }
53
+ ```
54
+
55
+ That's it — four or five primary tabs, an overflow sheet for the rest, and a theme you can
56
+ override with CSS custom properties.
57
+
58
+ See [`example/`](./example) for a complete, runnable demo (`pnpm example`).
59
+
60
+ ## Why a `LinkComponent` prop instead of just using `<a>`?
61
+
62
+ So client-side navigation works with whatever router you're on. Most router `Link` components
63
+ accept `href`, `className` and `children` — the same shape this package expects — so passing
64
+ `next/link`'s `Link`, React Router's `Link`, or TanStack Router's `Link` directly just works. If
65
+ you don't pass one, it falls back to a plain `<a href>`.
66
+
67
+ ## API
68
+
69
+ ### `<FloatingDock />`
70
+
71
+ | Prop | Type | Default | Description |
72
+ | ----------------- | -------------------------------------- | -------------- | ----------------------------------------------------------------------------- |
73
+ | `items` | `DockItem[]` | — | Destinations shown directly in the bar. Four or five is the sweet spot. |
74
+ | `moreItems` | `DockItem[]` | `[]` | Extra destinations shown in the overflow sheet. Omit to hide the "More" button entirely. |
75
+ | `isActive` | `(href: string) => boolean` | — | Tells the dock which item (if any) is the current page. |
76
+ | `LinkComponent` | `ComponentType<DockLinkProps>` | plain `<a>` | Your router's Link component. |
77
+ | `moreLabel` | `string` | `"More"` | Label under the overflow button. |
78
+ | `moreSheetTitle` | `string` | `"Menu"` | Heading inside the overflow sheet. |
79
+ | `closeLabel` | `string` | `"Close"` | `aria-label` for the sheet's close button. |
80
+ | `extraContent` | `ReactNode` | — | Rendered below the overflow grid — a natural slot for a theme toggle, sign-out button, language switcher, etc. |
81
+ | `ariaLabel` | `string` | `"Main"` | `aria-label` on the `<nav>` element. |
82
+ | `className` | `string` | — | Extra class on the `<nav>`, alongside the built-in one. |
83
+
84
+ ```ts
85
+ interface DockItem {
86
+ key: string; // React key, usually the same as href
87
+ href: string;
88
+ label: string;
89
+ icon: ComponentType<{ className?: string }>; // any icon component — lucide-react, heroicons, your own SVGs
90
+ }
91
+ ```
92
+
93
+ ### `<DockDrawer />`
94
+
95
+ The bottom sheet `FloatingDock` uses internally for its overflow menu. Exported separately because
96
+ it's useful on its own for any mobile-style sheet (filters, a confirmation prompt, etc.).
97
+
98
+ | Prop | Type | Default | Description |
99
+ | ------------ | ------------ | --------- | ----------------------------------- |
100
+ | `open` | `boolean` | — | Whether the sheet is visible. |
101
+ | `onClose` | `() => void` | — | Called on backdrop click, Escape, or the close button. |
102
+ | `title` | `string` | — | Heading shown in the sheet's header. |
103
+ | `children` | `ReactNode` | — | Sheet body. |
104
+ | `closeLabel` | `string` | `"Close"` | `aria-label` for the close button. |
105
+
106
+ ## Styling
107
+
108
+ Import `react-floating-dock/style.css` once, anywhere in your app. It's plain CSS driven entirely
109
+ by custom properties, so no Tailwind (or any other build-time CSS tooling) is required.
110
+
111
+ To theme it, redefine the variables wherever makes sense for your app — `:root` for a global
112
+ theme, or a wrapper element for something scoped:
113
+
114
+ ```css
115
+ :root {
116
+ --rfd-bg: rgba(255, 255, 255, 0.9); /* bar + sheet background */
117
+ --rfd-bg-solid: #ffffff; /* sheet background (no backdrop-filter) */
118
+ --rfd-border: #e2e8f0;
119
+ --rfd-text: #0f172a;
120
+ --rfd-text-muted: #64748b;
121
+ --rfd-accent: #3b82f6; /* active tab color */
122
+ --rfd-accent-bg: rgba(59, 130, 246, 0.12); /* active tab's pill background */
123
+ --rfd-accent-contrast: #ffffff; /* text/icon color on top of --rfd-accent */
124
+ --rfd-hover: rgba(0, 0, 0, 0.05);
125
+ --rfd-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
126
+ --rfd-radius: 9999px; /* fully rounded by default — set a smaller value for softer corners */
127
+ }
128
+ ```
129
+
130
+ All of the dock's classes are prefixed `rfd-` to avoid collisions with the rest of your app's CSS.
131
+
132
+ ## Accessibility
133
+
134
+ - The active tab gets `aria-current="page"`.
135
+ - The overflow sheet is a `role="dialog"` with `aria-modal="true"`, traps body scroll while open,
136
+ and closes on Escape.
137
+ - Every interactive element is a real `<button>` or your `LinkComponent`, so it's reachable and
138
+ operable by keyboard out of the box.
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ pnpm install
144
+ pnpm test # vitest
145
+ pnpm typecheck
146
+ pnpm lint
147
+ pnpm build # outputs dist/ (ESM + CJS + types + style.css)
148
+ pnpm example # runs the example app against the live source, at localhost:5173
149
+ ```
150
+
151
+ ## License
152
+
153
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var framerMotion = require('framer-motion');
5
+ var reactDom = require('react-dom');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/FloatingDock.tsx
9
+ function MoreHorizontalIcon(props) {
10
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11
+ "svg",
12
+ {
13
+ viewBox: "0 0 24 24",
14
+ width: "24",
15
+ height: "24",
16
+ fill: "none",
17
+ stroke: "currentColor",
18
+ strokeWidth: "2",
19
+ strokeLinecap: "round",
20
+ strokeLinejoin: "round",
21
+ "aria-hidden": "true",
22
+ ...props,
23
+ children: [
24
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1" }),
25
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "12", r: "1" }),
26
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "12", r: "1" })
27
+ ]
28
+ }
29
+ );
30
+ }
31
+ function CloseIcon(props) {
32
+ return /* @__PURE__ */ jsxRuntime.jsxs(
33
+ "svg",
34
+ {
35
+ viewBox: "0 0 24 24",
36
+ width: "24",
37
+ height: "24",
38
+ fill: "none",
39
+ stroke: "currentColor",
40
+ strokeWidth: "2",
41
+ strokeLinecap: "round",
42
+ strokeLinejoin: "round",
43
+ "aria-hidden": "true",
44
+ ...props,
45
+ children: [
46
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18" }),
47
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 6 12 12" })
48
+ ]
49
+ }
50
+ );
51
+ }
52
+ function DockDrawer({ open, onClose, title, children, closeLabel = "Close" }) {
53
+ const [mounted, setMounted] = react.useState(false);
54
+ react.useEffect(() => setMounted(true), []);
55
+ react.useEffect(() => {
56
+ if (!open) return;
57
+ const previousOverflow = document.body.style.overflow;
58
+ document.body.style.overflow = "hidden";
59
+ return () => {
60
+ document.body.style.overflow = previousOverflow;
61
+ };
62
+ }, [open]);
63
+ react.useEffect(() => {
64
+ if (!open) return;
65
+ const onKey = (event) => {
66
+ if (event.key === "Escape") onClose();
67
+ };
68
+ document.addEventListener("keydown", onKey);
69
+ return () => document.removeEventListener("keydown", onKey);
70
+ }, [open, onClose]);
71
+ if (!mounted) return null;
72
+ return reactDom.createPortal(
73
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-overlay-wrap", children: [
74
+ /* @__PURE__ */ jsxRuntime.jsx(
75
+ framerMotion.motion.div,
76
+ {
77
+ className: "rfd-overlay",
78
+ initial: { opacity: 0 },
79
+ animate: { opacity: 1 },
80
+ exit: { opacity: 0 },
81
+ onClick: onClose
82
+ }
83
+ ),
84
+ /* @__PURE__ */ jsxRuntime.jsxs(
85
+ framerMotion.motion.div,
86
+ {
87
+ role: "dialog",
88
+ "aria-modal": "true",
89
+ "aria-label": title,
90
+ className: "rfd-sheet",
91
+ initial: { y: "100%" },
92
+ animate: { y: 0 },
93
+ exit: { y: "100%" },
94
+ transition: { type: "spring", damping: 28, stiffness: 300 },
95
+ children: [
96
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-sheet-header", children: [
97
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "rfd-sheet-title", children: title }),
98
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onClose, "aria-label": closeLabel, className: "rfd-close-btn", children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "rfd-close-icon" }) })
99
+ ] }),
100
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfd-sheet-body", children })
101
+ ]
102
+ }
103
+ )
104
+ ] }) }),
105
+ document.body
106
+ );
107
+ }
108
+ var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, ...rest, children });
109
+ function FloatingDock({
110
+ items,
111
+ moreItems = [],
112
+ isActive,
113
+ LinkComponent = DefaultLink,
114
+ moreLabel = "More",
115
+ moreSheetTitle = "Menu",
116
+ closeLabel = "Close",
117
+ extraContent,
118
+ ariaLabel = "Main",
119
+ className
120
+ }) {
121
+ const [moreOpen, setMoreOpen] = react.useState(false);
122
+ const hasMore = moreItems.length > 0;
123
+ const moreActive = hasMore && moreItems.some((item) => isActive(item.href));
124
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
125
+ /* @__PURE__ */ jsxRuntime.jsx(
126
+ "nav",
127
+ {
128
+ "aria-label": ariaLabel,
129
+ className: ["rfd-bar", className].filter(Boolean).join(" "),
130
+ style: { bottom: "max(0.75rem, env(safe-area-inset-bottom))" },
131
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-track", children: [
132
+ items.map(({ key, href, label, icon: Icon }) => {
133
+ const active = isActive(href);
134
+ return /* @__PURE__ */ jsxRuntime.jsxs(LinkComponent, { href, className: "rfd-tab", "aria-current": active ? "page" : void 0, children: [
135
+ active && /* @__PURE__ */ jsxRuntime.jsx(
136
+ framerMotion.motion.span,
137
+ {
138
+ layoutId: "rfd-pill",
139
+ transition: { type: "spring", stiffness: 260, damping: 26 },
140
+ className: "rfd-pill"
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: active ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
144
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: active ? "rfd-label rfd-label--active" : "rfd-label", children: label })
145
+ ] }, key);
146
+ }),
147
+ hasMore && /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => setMoreOpen(true), "aria-label": moreLabel, className: "rfd-tab rfd-more-btn", children: [
148
+ moreActive && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rfd-pill" }),
149
+ /* @__PURE__ */ jsxRuntime.jsx(MoreHorizontalIcon, { className: moreActive ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
150
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: moreActive ? "rfd-label rfd-label--active" : "rfd-label", children: moreLabel })
151
+ ] })
152
+ ] })
153
+ }
154
+ ),
155
+ hasMore && /* @__PURE__ */ jsxRuntime.jsxs(DockDrawer, { open: moreOpen, onClose: () => setMoreOpen(false), title: moreSheetTitle, closeLabel, children: [
156
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfd-more-grid", children: moreItems.map(({ key, href, label, icon: Icon }) => {
157
+ const active = isActive(href);
158
+ return /* @__PURE__ */ jsxRuntime.jsxs(LinkComponent, { href, className: "rfd-more-item", onClick: () => setMoreOpen(false), children: [
159
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: active ? "rfd-more-icon-circle rfd-more-icon-circle--active" : "rfd-more-icon-circle", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "rfd-more-icon" }) }),
160
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rfd-more-label", children: label })
161
+ ] }, key);
162
+ }) }),
163
+ extraContent
164
+ ] })
165
+ ] });
166
+ }
167
+
168
+ exports.DockDrawer = DockDrawer;
169
+ exports.FloatingDock = FloatingDock;
170
+ //# sourceMappingURL=index.cjs.map
171
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/icons.tsx","../src/DockDrawer.tsx","../src/FloatingDock.tsx"],"names":["jsxs","jsx","useState","useEffect","createPortal","AnimatePresence","motion","Fragment"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { SVGProps } from \"react\";\n\n/** Tiny built-in icons so the package has zero icon-library dependency. */\n\nexport function MoreHorizontalIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </svg>\n );\n}\n\nexport function CloseIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { CloseIcon } from \"./icons\";\n\nexport interface DockDrawerProps {\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n /** aria-label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}\n * for its overflow menu, but works standalone for any mobile-style sheet.\n */\nexport function DockDrawer({ open, onClose, title, children, closeLabel = \"Close\" }: DockDrawerProps) {\n // Portals need a DOM node, which doesn't exist during SSR — render nothing until mounted client-side.\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!mounted) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <div className=\"rfd-overlay-wrap\">\n <motion.div\n className=\"rfd-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n onClick={onClose}\n />\n <motion.div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n className=\"rfd-sheet\"\n initial={{ y: \"100%\" }}\n animate={{ y: 0 }}\n exit={{ y: \"100%\" }}\n transition={{ type: \"spring\", damping: 28, stiffness: 300 }}\n >\n <div className=\"rfd-sheet-header\">\n <h2 className=\"rfd-sheet-title\">{title}</h2>\n <button type=\"button\" onClick={onClose} aria-label={closeLabel} className=\"rfd-close-btn\">\n <CloseIcon className=\"rfd-close-icon\" />\n </button>\n </div>\n <div className=\"rfd-sheet-body\">{children}</div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>,\n document.body,\n );\n}\n","\"use client\";\n\nimport { useState, type ComponentType, type ReactNode } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { DockDrawer } from \"./DockDrawer\";\nimport { MoreHorizontalIcon } from \"./icons\";\n\nexport interface DockItem {\n /** Unique React key. Usually the same as `href`. */\n key: string;\n href: string;\n label: string;\n icon: ComponentType<{ className?: string }>;\n}\n\nexport interface DockLinkProps {\n href: string;\n className?: string;\n children?: ReactNode;\n onClick?: () => void;\n \"aria-current\"?: \"page\";\n}\n\nexport interface FloatingDockProps {\n /** Destinations shown directly in the bar. Four or five is the sweet spot. */\n items: DockItem[];\n /** Extra destinations shown in the overflow sheet when the \"More\" button is pressed. */\n moreItems?: DockItem[];\n /** Tells the dock which item (if any) is the current page. */\n isActive: (href: string) => boolean;\n /**\n * The component used to render each navigable item — swap in your router's Link\n * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.\n * @default a plain anchor tag\n */\n LinkComponent?: ComponentType<DockLinkProps>;\n /** @default \"More\" */\n moreLabel?: string;\n /** @default \"Menu\" */\n moreSheetTitle?: string;\n /** Passed through to the overflow sheet's close button. @default \"Close\" */\n closeLabel?: string;\n /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */\n extraContent?: ReactNode;\n /** @default \"Main\" */\n ariaLabel?: string;\n className?: string;\n}\n\nconst DefaultLink: ComponentType<DockLinkProps> = ({ href, children, ...rest }) => (\n <a href={href} {...rest}>\n {children}\n </a>\n);\n\n/**\n * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an\n * animated pill highlights the active tab, and a \"More\" button opens the rest of your destinations\n * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.\n */\nexport function FloatingDock({\n items,\n moreItems = [],\n isActive,\n LinkComponent = DefaultLink,\n moreLabel = \"More\",\n moreSheetTitle = \"Menu\",\n closeLabel = \"Close\",\n extraContent,\n ariaLabel = \"Main\",\n className,\n}: FloatingDockProps) {\n const [moreOpen, setMoreOpen] = useState(false);\n const hasMore = moreItems.length > 0;\n const moreActive = hasMore && moreItems.some((item) => isActive(item.href));\n\n return (\n <>\n <nav\n aria-label={ariaLabel}\n className={[\"rfd-bar\", className].filter(Boolean).join(\" \")}\n style={{ bottom: \"max(0.75rem, env(safe-area-inset-bottom))\" }}\n >\n <div className=\"rfd-track\">\n {items.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-tab\" aria-current={active ? \"page\" : undefined}>\n {active && (\n <motion.span\n layoutId=\"rfd-pill\"\n transition={{ type: \"spring\", stiffness: 260, damping: 26 }}\n className=\"rfd-pill\"\n />\n )}\n <Icon className={active ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={active ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{label}</span>\n </LinkComponent>\n );\n })}\n\n {hasMore && (\n <button type=\"button\" onClick={() => setMoreOpen(true)} aria-label={moreLabel} className=\"rfd-tab rfd-more-btn\">\n {moreActive && <span className=\"rfd-pill\" />}\n <MoreHorizontalIcon className={moreActive ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={moreActive ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{moreLabel}</span>\n </button>\n )}\n </div>\n </nav>\n\n {hasMore && (\n <DockDrawer open={moreOpen} onClose={() => setMoreOpen(false)} title={moreSheetTitle} closeLabel={closeLabel}>\n <div className=\"rfd-more-grid\">\n {moreItems.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-more-item\" onClick={() => setMoreOpen(false)}>\n <span className={active ? \"rfd-more-icon-circle rfd-more-icon-circle--active\" : \"rfd-more-icon-circle\"}>\n <Icon className=\"rfd-more-icon\" />\n </span>\n <span className=\"rfd-more-label\">{label}</span>\n </LinkComponent>\n );\n })}\n </div>\n {extraContent}\n </DockDrawer>\n )}\n </>\n );\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+
4
+ interface DockItem {
5
+ /** Unique React key. Usually the same as `href`. */
6
+ key: string;
7
+ href: string;
8
+ label: string;
9
+ icon: ComponentType<{
10
+ className?: string;
11
+ }>;
12
+ }
13
+ interface DockLinkProps {
14
+ href: string;
15
+ className?: string;
16
+ children?: ReactNode;
17
+ onClick?: () => void;
18
+ "aria-current"?: "page";
19
+ }
20
+ interface FloatingDockProps {
21
+ /** Destinations shown directly in the bar. Four or five is the sweet spot. */
22
+ items: DockItem[];
23
+ /** Extra destinations shown in the overflow sheet when the "More" button is pressed. */
24
+ moreItems?: DockItem[];
25
+ /** Tells the dock which item (if any) is the current page. */
26
+ isActive: (href: string) => boolean;
27
+ /**
28
+ * The component used to render each navigable item — swap in your router's Link
29
+ * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.
30
+ * @default a plain anchor tag
31
+ */
32
+ LinkComponent?: ComponentType<DockLinkProps>;
33
+ /** @default "More" */
34
+ moreLabel?: string;
35
+ /** @default "Menu" */
36
+ moreSheetTitle?: string;
37
+ /** Passed through to the overflow sheet's close button. @default "Close" */
38
+ closeLabel?: string;
39
+ /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */
40
+ extraContent?: ReactNode;
41
+ /** @default "Main" */
42
+ ariaLabel?: string;
43
+ className?: string;
44
+ }
45
+ /**
46
+ * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an
47
+ * animated pill highlights the active tab, and a "More" button opens the rest of your destinations
48
+ * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.
49
+ */
50
+ declare function FloatingDock({ items, moreItems, isActive, LinkComponent, moreLabel, moreSheetTitle, closeLabel, extraContent, ariaLabel, className, }: FloatingDockProps): react.JSX.Element;
51
+
52
+ interface DockDrawerProps {
53
+ open: boolean;
54
+ onClose: () => void;
55
+ title: string;
56
+ children: ReactNode;
57
+ /** aria-label for the close button. @default "Close" */
58
+ closeLabel?: string;
59
+ }
60
+ /**
61
+ * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}
62
+ * for its overflow menu, but works standalone for any mobile-style sheet.
63
+ */
64
+ declare function DockDrawer({ open, onClose, title, children, closeLabel }: DockDrawerProps): react.ReactPortal | null;
65
+
66
+ export { DockDrawer, FloatingDock };
67
+ export type { DockDrawerProps, DockItem, DockLinkProps, FloatingDockProps };
@@ -0,0 +1,67 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+
4
+ interface DockItem {
5
+ /** Unique React key. Usually the same as `href`. */
6
+ key: string;
7
+ href: string;
8
+ label: string;
9
+ icon: ComponentType<{
10
+ className?: string;
11
+ }>;
12
+ }
13
+ interface DockLinkProps {
14
+ href: string;
15
+ className?: string;
16
+ children?: ReactNode;
17
+ onClick?: () => void;
18
+ "aria-current"?: "page";
19
+ }
20
+ interface FloatingDockProps {
21
+ /** Destinations shown directly in the bar. Four or five is the sweet spot. */
22
+ items: DockItem[];
23
+ /** Extra destinations shown in the overflow sheet when the "More" button is pressed. */
24
+ moreItems?: DockItem[];
25
+ /** Tells the dock which item (if any) is the current page. */
26
+ isActive: (href: string) => boolean;
27
+ /**
28
+ * The component used to render each navigable item — swap in your router's Link
29
+ * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.
30
+ * @default a plain anchor tag
31
+ */
32
+ LinkComponent?: ComponentType<DockLinkProps>;
33
+ /** @default "More" */
34
+ moreLabel?: string;
35
+ /** @default "Menu" */
36
+ moreSheetTitle?: string;
37
+ /** Passed through to the overflow sheet's close button. @default "Close" */
38
+ closeLabel?: string;
39
+ /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */
40
+ extraContent?: ReactNode;
41
+ /** @default "Main" */
42
+ ariaLabel?: string;
43
+ className?: string;
44
+ }
45
+ /**
46
+ * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an
47
+ * animated pill highlights the active tab, and a "More" button opens the rest of your destinations
48
+ * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.
49
+ */
50
+ declare function FloatingDock({ items, moreItems, isActive, LinkComponent, moreLabel, moreSheetTitle, closeLabel, extraContent, ariaLabel, className, }: FloatingDockProps): react.JSX.Element;
51
+
52
+ interface DockDrawerProps {
53
+ open: boolean;
54
+ onClose: () => void;
55
+ title: string;
56
+ children: ReactNode;
57
+ /** aria-label for the close button. @default "Close" */
58
+ closeLabel?: string;
59
+ }
60
+ /**
61
+ * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}
62
+ * for its overflow menu, but works standalone for any mobile-style sheet.
63
+ */
64
+ declare function DockDrawer({ open, onClose, title, children, closeLabel }: DockDrawerProps): react.ReactPortal | null;
65
+
66
+ export { DockDrawer, FloatingDock };
67
+ export type { DockDrawerProps, DockItem, DockLinkProps, FloatingDockProps };
package/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+
6
+ // src/FloatingDock.tsx
7
+ function MoreHorizontalIcon(props) {
8
+ return /* @__PURE__ */ jsxs(
9
+ "svg",
10
+ {
11
+ viewBox: "0 0 24 24",
12
+ width: "24",
13
+ height: "24",
14
+ fill: "none",
15
+ stroke: "currentColor",
16
+ strokeWidth: "2",
17
+ strokeLinecap: "round",
18
+ strokeLinejoin: "round",
19
+ "aria-hidden": "true",
20
+ ...props,
21
+ children: [
22
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1" }),
23
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1" }),
24
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1" })
25
+ ]
26
+ }
27
+ );
28
+ }
29
+ function CloseIcon(props) {
30
+ return /* @__PURE__ */ jsxs(
31
+ "svg",
32
+ {
33
+ viewBox: "0 0 24 24",
34
+ width: "24",
35
+ height: "24",
36
+ fill: "none",
37
+ stroke: "currentColor",
38
+ strokeWidth: "2",
39
+ strokeLinecap: "round",
40
+ strokeLinejoin: "round",
41
+ "aria-hidden": "true",
42
+ ...props,
43
+ children: [
44
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
45
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
46
+ ]
47
+ }
48
+ );
49
+ }
50
+ function DockDrawer({ open, onClose, title, children, closeLabel = "Close" }) {
51
+ const [mounted, setMounted] = useState(false);
52
+ useEffect(() => setMounted(true), []);
53
+ useEffect(() => {
54
+ if (!open) return;
55
+ const previousOverflow = document.body.style.overflow;
56
+ document.body.style.overflow = "hidden";
57
+ return () => {
58
+ document.body.style.overflow = previousOverflow;
59
+ };
60
+ }, [open]);
61
+ useEffect(() => {
62
+ if (!open) return;
63
+ const onKey = (event) => {
64
+ if (event.key === "Escape") onClose();
65
+ };
66
+ document.addEventListener("keydown", onKey);
67
+ return () => document.removeEventListener("keydown", onKey);
68
+ }, [open, onClose]);
69
+ if (!mounted) return null;
70
+ return createPortal(
71
+ /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs("div", { className: "rfd-overlay-wrap", children: [
72
+ /* @__PURE__ */ jsx(
73
+ motion.div,
74
+ {
75
+ className: "rfd-overlay",
76
+ initial: { opacity: 0 },
77
+ animate: { opacity: 1 },
78
+ exit: { opacity: 0 },
79
+ onClick: onClose
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsxs(
83
+ motion.div,
84
+ {
85
+ role: "dialog",
86
+ "aria-modal": "true",
87
+ "aria-label": title,
88
+ className: "rfd-sheet",
89
+ initial: { y: "100%" },
90
+ animate: { y: 0 },
91
+ exit: { y: "100%" },
92
+ transition: { type: "spring", damping: 28, stiffness: 300 },
93
+ children: [
94
+ /* @__PURE__ */ jsxs("div", { className: "rfd-sheet-header", children: [
95
+ /* @__PURE__ */ jsx("h2", { className: "rfd-sheet-title", children: title }),
96
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, "aria-label": closeLabel, className: "rfd-close-btn", children: /* @__PURE__ */ jsx(CloseIcon, { className: "rfd-close-icon" }) })
97
+ ] }),
98
+ /* @__PURE__ */ jsx("div", { className: "rfd-sheet-body", children })
99
+ ]
100
+ }
101
+ )
102
+ ] }) }),
103
+ document.body
104
+ );
105
+ }
106
+ var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsx("a", { href, ...rest, children });
107
+ function FloatingDock({
108
+ items,
109
+ moreItems = [],
110
+ isActive,
111
+ LinkComponent = DefaultLink,
112
+ moreLabel = "More",
113
+ moreSheetTitle = "Menu",
114
+ closeLabel = "Close",
115
+ extraContent,
116
+ ariaLabel = "Main",
117
+ className
118
+ }) {
119
+ const [moreOpen, setMoreOpen] = useState(false);
120
+ const hasMore = moreItems.length > 0;
121
+ const moreActive = hasMore && moreItems.some((item) => isActive(item.href));
122
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsx(
124
+ "nav",
125
+ {
126
+ "aria-label": ariaLabel,
127
+ className: ["rfd-bar", className].filter(Boolean).join(" "),
128
+ style: { bottom: "max(0.75rem, env(safe-area-inset-bottom))" },
129
+ children: /* @__PURE__ */ jsxs("div", { className: "rfd-track", children: [
130
+ items.map(({ key, href, label, icon: Icon }) => {
131
+ const active = isActive(href);
132
+ return /* @__PURE__ */ jsxs(LinkComponent, { href, className: "rfd-tab", "aria-current": active ? "page" : void 0, children: [
133
+ active && /* @__PURE__ */ jsx(
134
+ motion.span,
135
+ {
136
+ layoutId: "rfd-pill",
137
+ transition: { type: "spring", stiffness: 260, damping: 26 },
138
+ className: "rfd-pill"
139
+ }
140
+ ),
141
+ /* @__PURE__ */ jsx(Icon, { className: active ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
142
+ /* @__PURE__ */ jsx("span", { className: active ? "rfd-label rfd-label--active" : "rfd-label", children: label })
143
+ ] }, key);
144
+ }),
145
+ hasMore && /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setMoreOpen(true), "aria-label": moreLabel, className: "rfd-tab rfd-more-btn", children: [
146
+ moreActive && /* @__PURE__ */ jsx("span", { className: "rfd-pill" }),
147
+ /* @__PURE__ */ jsx(MoreHorizontalIcon, { className: moreActive ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
148
+ /* @__PURE__ */ jsx("span", { className: moreActive ? "rfd-label rfd-label--active" : "rfd-label", children: moreLabel })
149
+ ] })
150
+ ] })
151
+ }
152
+ ),
153
+ hasMore && /* @__PURE__ */ jsxs(DockDrawer, { open: moreOpen, onClose: () => setMoreOpen(false), title: moreSheetTitle, closeLabel, children: [
154
+ /* @__PURE__ */ jsx("div", { className: "rfd-more-grid", children: moreItems.map(({ key, href, label, icon: Icon }) => {
155
+ const active = isActive(href);
156
+ return /* @__PURE__ */ jsxs(LinkComponent, { href, className: "rfd-more-item", onClick: () => setMoreOpen(false), children: [
157
+ /* @__PURE__ */ jsx("span", { className: active ? "rfd-more-icon-circle rfd-more-icon-circle--active" : "rfd-more-icon-circle", children: /* @__PURE__ */ jsx(Icon, { className: "rfd-more-icon" }) }),
158
+ /* @__PURE__ */ jsx("span", { className: "rfd-more-label", children: label })
159
+ ] }, key);
160
+ }) }),
161
+ extraContent
162
+ ] })
163
+ ] });
164
+ }
165
+
166
+ export { DockDrawer, FloatingDock };
167
+ //# sourceMappingURL=index.js.map
168
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/icons.tsx","../src/DockDrawer.tsx","../src/FloatingDock.tsx"],"names":["jsx","jsxs","useState","motion"],"mappings":";;;;;;AAIO,SAAS,mBAAmB,KAAA,EAAgC;AACjE,EAAA,uBACE,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,cAAA;AAAA,MACP,WAAA,EAAY,GAAA;AAAA,MACZ,aAAA,EAAc,OAAA;AAAA,MACd,cAAA,EAAe,OAAA;AAAA,MACf,aAAA,EAAY,MAAA;AAAA,MACX,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,YAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI,CAAA;AAAA,4BAC7B,QAAA,EAAA,EAAO,EAAA,EAAG,MAAK,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI,CAAA;AAAA,4BAC7B,QAAA,EAAA,EAAO,EAAA,EAAG,KAAI,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI;AAAA;AAAA;AAAA,GAC/B;AAEJ;AAEO,SAAS,UAAU,KAAA,EAAgC;AACxD,EAAA,uBACE,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,cAAA;AAAA,MACP,WAAA,EAAY,GAAA;AAAA,MACZ,aAAA,EAAc,OAAA;AAAA,MACd,cAAA,EAAe,OAAA;AAAA,MACf,aAAA,EAAY,MAAA;AAAA,MACX,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,GAAE,YAAA,EAAa,CAAA;AAAA,wBACrB,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,YAAA,EAAa;AAAA;AAAA;AAAA,GACvB;AAEJ;ACvBO,SAAS,UAAA,CAAW,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,QAAA,EAAU,UAAA,GAAa,SAAQ,EAAoB;AAEpG,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,SAAA,CAAU,MAAM,UAAA,CAAW,IAAI,CAAA,EAAG,EAAE,CAAA;AAEpC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,MAAM,gBAAA,GAAmB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AAC7C,IAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,gBAAA;AAAA,IACjC,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,MAAM,KAAA,GAAQ,CAAC,KAAA,KAAyB;AACtC,MAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,QAAA,EAAU,OAAA,EAAQ;AAAA,IACtC,CAAA;AACA,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,KAAK,CAAA;AAC1C,IAAA,OAAO,MAAM,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,KAAK,CAAA;AAAA,EAC5D,CAAA,EAAG,CAAC,IAAA,EAAM,OAAO,CAAC,CAAA;AAElB,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,EAAA,OAAO,YAAA;AAAA,oBACLA,IAAC,eAAA,EAAA,EACE,QAAA,EAAA,IAAA,oBACCC,IAAAA,CAAC,KAAA,EAAA,EAAI,WAAU,kBAAA,EACb,QAAA,EAAA;AAAA,sBAAAD,GAAAA;AAAA,QAAC,MAAA,CAAO,GAAA;AAAA,QAAP;AAAA,UACC,SAAA,EAAU,aAAA;AAAA,UACV,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACtB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACtB,IAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACnB,OAAA,EAAS;AAAA;AAAA,OACX;AAAA,sBACAC,IAAAA;AAAA,QAAC,MAAA,CAAO,GAAA;AAAA,QAAP;AAAA,UACC,IAAA,EAAK,QAAA;AAAA,UACL,YAAA,EAAW,MAAA;AAAA,UACX,YAAA,EAAY,KAAA;AAAA,UACZ,SAAA,EAAU,WAAA;AAAA,UACV,OAAA,EAAS,EAAE,CAAA,EAAG,MAAA,EAAO;AAAA,UACrB,OAAA,EAAS,EAAE,CAAA,EAAG,CAAA,EAAE;AAAA,UAChB,IAAA,EAAM,EAAE,CAAA,EAAG,MAAA,EAAO;AAAA,UAClB,YAAY,EAAE,IAAA,EAAM,UAAU,OAAA,EAAS,EAAA,EAAI,WAAW,GAAA,EAAI;AAAA,UAE1D,QAAA,EAAA;AAAA,4BAAAA,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EACb,QAAA,EAAA;AAAA,8BAAAD,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,iBAAA,EAAmB,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,8BACvCA,GAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,SAAS,OAAA,EAAS,YAAA,EAAY,UAAA,EAAY,SAAA,EAAU,iBACxE,QAAA,kBAAAA,GAAAA,CAAC,SAAA,EAAA,EAAU,SAAA,EAAU,kBAAiB,CAAA,EACxC;AAAA,aAAA,EACF,CAAA;AAAA,4BACAA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAkB,QAAA,EAAS;AAAA;AAAA;AAAA;AAC5C,KAAA,EACF,CAAA,EAEJ,CAAA;AAAA,IACA,QAAA,CAAS;AAAA,GACX;AACF;AC9BA,IAAM,WAAA,GAA4C,CAAC,EAAE,IAAA,EAAM,UAAU,GAAG,IAAA,EAAK,qBAC3EA,GAAAA,CAAC,GAAA,EAAA,EAAE,IAAA,EAAa,GAAG,MAChB,QAAA,EACH,CAAA;AAQK,SAAS,YAAA,CAAa;AAAA,EAC3B,KAAA;AAAA,EACA,YAAY,EAAC;AAAA,EACb,QAAA;AAAA,EACA,aAAA,GAAgB,WAAA;AAAA,EAChB,SAAA,GAAY,MAAA;AAAA,EACZ,cAAA,GAAiB,MAAA;AAAA,EACjB,UAAA,GAAa,OAAA;AAAA,EACb,YAAA;AAAA,EACA,SAAA,GAAY,MAAA;AAAA,EACZ;AACF,CAAA,EAAsB;AACpB,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIE,SAAS,KAAK,CAAA;AAC9C,EAAA,MAAM,OAAA,GAAU,UAAU,MAAA,GAAS,CAAA;AACnC,EAAA,MAAM,UAAA,GAAa,WAAW,SAAA,CAAU,IAAA,CAAK,CAAC,IAAA,KAAS,QAAA,CAAS,IAAA,CAAK,IAAI,CAAC,CAAA;AAE1E,EAAA,uBACED,KAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAAD,GAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,YAAA,EAAY,SAAA;AAAA,QACZ,SAAA,EAAW,CAAC,SAAA,EAAW,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,QAC1D,KAAA,EAAO,EAAE,MAAA,EAAQ,2CAAA,EAA4C;AAAA,QAE7D,QAAA,kBAAAC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,WAAA,EACZ,QAAA,EAAA;AAAA,UAAA,KAAA,CAAM,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,MAAM,KAAA,EAAO,IAAA,EAAM,MAAK,KAAM;AAC/C,YAAA,MAAM,MAAA,GAAS,SAAS,IAAI,CAAA;AAC5B,YAAA,uBACEA,KAAC,aAAA,EAAA,EAAwB,IAAA,EAAY,WAAU,SAAA,EAAU,cAAA,EAAc,MAAA,GAAS,MAAA,GAAS,MAAA,EACtF,QAAA,EAAA;AAAA,cAAA,MAAA,oBACCD,GAAAA;AAAA,gBAACG,MAAAA,CAAO,IAAA;AAAA,gBAAP;AAAA,kBACC,QAAA,EAAS,UAAA;AAAA,kBACT,YAAY,EAAE,IAAA,EAAM,UAAU,SAAA,EAAW,GAAA,EAAK,SAAS,EAAA,EAAG;AAAA,kBAC1D,SAAA,EAAU;AAAA;AAAA,eACZ;AAAA,8BAEFH,GAAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAW,MAAA,GAAS,8BAA8B,UAAA,EAAY,CAAA;AAAA,8BACpEA,GAAAA,CAAC,MAAA,EAAA,EAAK,WAAW,MAAA,GAAS,6BAAA,GAAgC,aAAc,QAAA,EAAA,KAAA,EAAM;AAAA,aAAA,EAAA,EAT5D,GAUpB,CAAA;AAAA,UAEJ,CAAC,CAAA;AAAA,UAEA,OAAA,oBACCC,IAAAA,CAAC,QAAA,EAAA,EAAO,MAAK,QAAA,EAAS,OAAA,EAAS,MAAM,WAAA,CAAY,IAAI,CAAA,EAAG,YAAA,EAAY,SAAA,EAAW,WAAU,sBAAA,EACtF,QAAA,EAAA;AAAA,YAAA,UAAA,oBAAcD,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,UAAA,EAAW,CAAA;AAAA,4BAC1CA,GAAAA,CAAC,kBAAA,EAAA,EAAmB,SAAA,EAAW,UAAA,GAAa,8BAA8B,UAAA,EAAY,CAAA;AAAA,4BACtFA,GAAAA,CAAC,MAAA,EAAA,EAAK,WAAW,UAAA,GAAa,6BAAA,GAAgC,aAAc,QAAA,EAAA,SAAA,EAAU;AAAA,WAAA,EACxF;AAAA,SAAA,EAEJ;AAAA;AAAA,KACF;AAAA,IAEC,OAAA,oBACCC,IAAAA,CAAC,UAAA,EAAA,EAAW,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,MAAM,WAAA,CAAY,KAAK,CAAA,EAAG,KAAA,EAAO,gBAAgB,UAAA,EACpF,QAAA,EAAA;AAAA,sBAAAD,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,eAAA,EACZ,QAAA,EAAA,SAAA,CAAU,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,MAAK,KAAM;AACnD,QAAA,MAAM,MAAA,GAAS,SAAS,IAAI,CAAA;AAC5B,QAAA,uBACEC,IAAAA,CAAC,aAAA,EAAA,EAAwB,IAAA,EAAY,SAAA,EAAU,iBAAgB,OAAA,EAAS,MAAM,WAAA,CAAY,KAAK,CAAA,EAC7F,QAAA,EAAA;AAAA,0BAAAD,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,GAAS,mDAAA,GAAsD,sBAAA,EAC9E,QAAA,kBAAAA,GAAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EAAgB,CAAA,EAClC,CAAA;AAAA,0BACAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,kBAAkB,QAAA,EAAA,KAAA,EAAM;AAAA,SAAA,EAAA,EAJtB,GAKpB,CAAA;AAAA,MAEJ,CAAC,CAAA,EACH,CAAA;AAAA,MACC;AAAA,KAAA,EACH;AAAA,GAAA,EAEJ,CAAA;AAEJ","file":"index.js","sourcesContent":["import type { SVGProps } from \"react\";\n\n/** Tiny built-in icons so the package has zero icon-library dependency. */\n\nexport function MoreHorizontalIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </svg>\n );\n}\n\nexport function CloseIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { CloseIcon } from \"./icons\";\n\nexport interface DockDrawerProps {\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n /** aria-label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}\n * for its overflow menu, but works standalone for any mobile-style sheet.\n */\nexport function DockDrawer({ open, onClose, title, children, closeLabel = \"Close\" }: DockDrawerProps) {\n // Portals need a DOM node, which doesn't exist during SSR — render nothing until mounted client-side.\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!mounted) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <div className=\"rfd-overlay-wrap\">\n <motion.div\n className=\"rfd-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n onClick={onClose}\n />\n <motion.div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n className=\"rfd-sheet\"\n initial={{ y: \"100%\" }}\n animate={{ y: 0 }}\n exit={{ y: \"100%\" }}\n transition={{ type: \"spring\", damping: 28, stiffness: 300 }}\n >\n <div className=\"rfd-sheet-header\">\n <h2 className=\"rfd-sheet-title\">{title}</h2>\n <button type=\"button\" onClick={onClose} aria-label={closeLabel} className=\"rfd-close-btn\">\n <CloseIcon className=\"rfd-close-icon\" />\n </button>\n </div>\n <div className=\"rfd-sheet-body\">{children}</div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>,\n document.body,\n );\n}\n","\"use client\";\n\nimport { useState, type ComponentType, type ReactNode } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { DockDrawer } from \"./DockDrawer\";\nimport { MoreHorizontalIcon } from \"./icons\";\n\nexport interface DockItem {\n /** Unique React key. Usually the same as `href`. */\n key: string;\n href: string;\n label: string;\n icon: ComponentType<{ className?: string }>;\n}\n\nexport interface DockLinkProps {\n href: string;\n className?: string;\n children?: ReactNode;\n onClick?: () => void;\n \"aria-current\"?: \"page\";\n}\n\nexport interface FloatingDockProps {\n /** Destinations shown directly in the bar. Four or five is the sweet spot. */\n items: DockItem[];\n /** Extra destinations shown in the overflow sheet when the \"More\" button is pressed. */\n moreItems?: DockItem[];\n /** Tells the dock which item (if any) is the current page. */\n isActive: (href: string) => boolean;\n /**\n * The component used to render each navigable item — swap in your router's Link\n * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.\n * @default a plain anchor tag\n */\n LinkComponent?: ComponentType<DockLinkProps>;\n /** @default \"More\" */\n moreLabel?: string;\n /** @default \"Menu\" */\n moreSheetTitle?: string;\n /** Passed through to the overflow sheet's close button. @default \"Close\" */\n closeLabel?: string;\n /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */\n extraContent?: ReactNode;\n /** @default \"Main\" */\n ariaLabel?: string;\n className?: string;\n}\n\nconst DefaultLink: ComponentType<DockLinkProps> = ({ href, children, ...rest }) => (\n <a href={href} {...rest}>\n {children}\n </a>\n);\n\n/**\n * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an\n * animated pill highlights the active tab, and a \"More\" button opens the rest of your destinations\n * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.\n */\nexport function FloatingDock({\n items,\n moreItems = [],\n isActive,\n LinkComponent = DefaultLink,\n moreLabel = \"More\",\n moreSheetTitle = \"Menu\",\n closeLabel = \"Close\",\n extraContent,\n ariaLabel = \"Main\",\n className,\n}: FloatingDockProps) {\n const [moreOpen, setMoreOpen] = useState(false);\n const hasMore = moreItems.length > 0;\n const moreActive = hasMore && moreItems.some((item) => isActive(item.href));\n\n return (\n <>\n <nav\n aria-label={ariaLabel}\n className={[\"rfd-bar\", className].filter(Boolean).join(\" \")}\n style={{ bottom: \"max(0.75rem, env(safe-area-inset-bottom))\" }}\n >\n <div className=\"rfd-track\">\n {items.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-tab\" aria-current={active ? \"page\" : undefined}>\n {active && (\n <motion.span\n layoutId=\"rfd-pill\"\n transition={{ type: \"spring\", stiffness: 260, damping: 26 }}\n className=\"rfd-pill\"\n />\n )}\n <Icon className={active ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={active ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{label}</span>\n </LinkComponent>\n );\n })}\n\n {hasMore && (\n <button type=\"button\" onClick={() => setMoreOpen(true)} aria-label={moreLabel} className=\"rfd-tab rfd-more-btn\">\n {moreActive && <span className=\"rfd-pill\" />}\n <MoreHorizontalIcon className={moreActive ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={moreActive ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{moreLabel}</span>\n </button>\n )}\n </div>\n </nav>\n\n {hasMore && (\n <DockDrawer open={moreOpen} onClose={() => setMoreOpen(false)} title={moreSheetTitle} closeLabel={closeLabel}>\n <div className=\"rfd-more-grid\">\n {moreItems.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-more-item\" onClick={() => setMoreOpen(false)}>\n <span className={active ? \"rfd-more-icon-circle rfd-more-icon-circle--active\" : \"rfd-more-icon-circle\"}>\n <Icon className=\"rfd-more-icon\" />\n </span>\n <span className=\"rfd-more-label\">{label}</span>\n </LinkComponent>\n );\n })}\n </div>\n {extraContent}\n </DockDrawer>\n )}\n </>\n );\n}\n"]}
package/dist/style.css ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * react-floating-dock default theme.
3
+ * Everything is driven by CSS custom properties — redefine any of these on `:root`
4
+ * (or scoped to a wrapper element) to theme the dock without touching these rules.
5
+ */
6
+ :root {
7
+ --rfd-bg: rgba(15, 23, 42, 0.9);
8
+ --rfd-bg-solid: #0f172a;
9
+ --rfd-border: #1e293b;
10
+ --rfd-text: #e2e8f0;
11
+ --rfd-text-muted: #94a3b8;
12
+ --rfd-accent: #3b82f6;
13
+ --rfd-accent-bg: rgba(59, 130, 246, 0.15);
14
+ --rfd-accent-contrast: #ffffff;
15
+ --rfd-hover: rgba(255, 255, 255, 0.06);
16
+ --rfd-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.35);
17
+ --rfd-radius: 9999px;
18
+ --rfd-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
19
+ }
20
+
21
+ .rfd-bar {
22
+ position: fixed;
23
+ left: 0.75rem;
24
+ right: 0.75rem;
25
+ z-index: 40;
26
+ font-family: var(--rfd-font);
27
+ }
28
+
29
+ .rfd-track {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ gap: 0.25rem;
34
+ border-radius: var(--rfd-radius);
35
+ border: 1px solid var(--rfd-border);
36
+ background: var(--rfd-bg);
37
+ backdrop-filter: blur(16px);
38
+ -webkit-backdrop-filter: blur(16px);
39
+ padding: 0.375rem;
40
+ box-shadow: var(--rfd-shadow);
41
+ }
42
+
43
+ .rfd-tab {
44
+ position: relative;
45
+ display: flex;
46
+ flex: 1 1 0%;
47
+ height: 3.5rem;
48
+ flex-direction: column;
49
+ align-items: center;
50
+ justify-content: center;
51
+ gap: 0.125rem;
52
+ border-radius: var(--rfd-radius);
53
+ background: transparent;
54
+ border: none;
55
+ padding: 0;
56
+ margin: 0;
57
+ cursor: pointer;
58
+ text-decoration: none;
59
+ color: inherit;
60
+ font: inherit;
61
+ -webkit-tap-highlight-color: transparent;
62
+ }
63
+
64
+ .rfd-pill {
65
+ position: absolute;
66
+ inset: 0;
67
+ border-radius: var(--rfd-radius);
68
+ background: var(--rfd-accent-bg);
69
+ }
70
+
71
+ .rfd-icon {
72
+ position: relative;
73
+ width: 1.25rem;
74
+ height: 1.25rem;
75
+ color: var(--rfd-text-muted);
76
+ flex-shrink: 0;
77
+ }
78
+ .rfd-icon--active {
79
+ color: var(--rfd-accent);
80
+ }
81
+
82
+ .rfd-label {
83
+ position: relative;
84
+ font-size: 11px;
85
+ line-height: 1;
86
+ font-weight: 500;
87
+ color: var(--rfd-text-muted);
88
+ white-space: nowrap;
89
+ }
90
+ .rfd-label--active {
91
+ color: var(--rfd-accent);
92
+ }
93
+
94
+ /* --- Overflow sheet --- */
95
+
96
+ .rfd-overlay-wrap {
97
+ position: fixed;
98
+ inset: 0;
99
+ z-index: 50;
100
+ display: flex;
101
+ align-items: flex-end;
102
+ justify-content: center;
103
+ font-family: var(--rfd-font);
104
+ }
105
+ @media (min-width: 640px) {
106
+ .rfd-overlay-wrap {
107
+ align-items: center;
108
+ }
109
+ }
110
+
111
+ .rfd-overlay {
112
+ position: absolute;
113
+ inset: 0;
114
+ background: rgba(0, 0, 0, 0.6);
115
+ }
116
+
117
+ .rfd-sheet {
118
+ position: relative;
119
+ z-index: 10;
120
+ width: 100%;
121
+ max-width: 32rem;
122
+ border-radius: 1.5rem 1.5rem 0 0;
123
+ border: 1px solid var(--rfd-border);
124
+ background: var(--rfd-bg-solid);
125
+ padding-bottom: env(safe-area-inset-bottom);
126
+ }
127
+ @media (min-width: 640px) {
128
+ .rfd-sheet {
129
+ border-radius: 1.5rem;
130
+ }
131
+ }
132
+
133
+ .rfd-sheet-header {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ border-bottom: 1px solid var(--rfd-border);
138
+ padding: 1rem 1.25rem;
139
+ }
140
+
141
+ .rfd-sheet-title {
142
+ font-size: 1rem;
143
+ font-weight: 600;
144
+ color: var(--rfd-text);
145
+ margin: 0;
146
+ }
147
+
148
+ .rfd-close-btn {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ width: 2.75rem;
153
+ height: 2.75rem;
154
+ border-radius: var(--rfd-radius);
155
+ border: none;
156
+ background: transparent;
157
+ color: var(--rfd-text-muted);
158
+ cursor: pointer;
159
+ padding: 0;
160
+ -webkit-tap-highlight-color: transparent;
161
+ }
162
+ .rfd-close-btn:hover {
163
+ background: var(--rfd-hover);
164
+ color: var(--rfd-text);
165
+ }
166
+ .rfd-close-icon {
167
+ width: 1.25rem;
168
+ height: 1.25rem;
169
+ }
170
+
171
+ .rfd-sheet-body {
172
+ max-height: 75vh;
173
+ overflow-y: auto;
174
+ padding: 1.25rem;
175
+ }
176
+
177
+ .rfd-more-grid {
178
+ display: grid;
179
+ grid-template-columns: repeat(3, 1fr);
180
+ gap: 1rem;
181
+ }
182
+
183
+ .rfd-more-item {
184
+ display: flex;
185
+ flex-direction: column;
186
+ align-items: center;
187
+ gap: 0.5rem;
188
+ text-align: center;
189
+ text-decoration: none;
190
+ color: inherit;
191
+ background: none;
192
+ border: none;
193
+ padding: 0;
194
+ cursor: pointer;
195
+ font: inherit;
196
+ -webkit-tap-highlight-color: transparent;
197
+ }
198
+
199
+ .rfd-more-icon-circle {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ width: 4rem;
204
+ height: 4rem;
205
+ border-radius: var(--rfd-radius);
206
+ background: var(--rfd-hover);
207
+ color: var(--rfd-text);
208
+ }
209
+ .rfd-more-icon-circle--active {
210
+ background: var(--rfd-accent);
211
+ color: var(--rfd-accent-contrast);
212
+ }
213
+
214
+ .rfd-more-icon {
215
+ width: 1.5rem;
216
+ height: 1.5rem;
217
+ }
218
+
219
+ .rfd-more-label {
220
+ font-size: 12px;
221
+ font-weight: 500;
222
+ color: var(--rfd-text);
223
+ }
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "react-floating-dock",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A WhatsApp-style floating bottom navigation dock for React, with an animated active-tab pill and an overflow sheet for extra destinations. Framework-agnostic — bring your own router.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./style.css": "./dist/style.css"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "sideEffects": [
21
+ "**/*.css"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup && node scripts/copy-css.mjs",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint .",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "example": "vite --config example/vite.config.ts",
31
+ "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
32
+ },
33
+ "keywords": [
34
+ "react",
35
+ "navigation",
36
+ "mobile",
37
+ "bottom-nav",
38
+ "tab-bar",
39
+ "pwa",
40
+ "dock",
41
+ "bottom-sheet"
42
+ ],
43
+ "author": "Lemon Oluwatobi",
44
+ "license": "MIT",
45
+ "peerDependencies": {
46
+ "framer-motion": ">=11",
47
+ "react": ">=18",
48
+ "react-dom": ">=18"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^10.0.1",
52
+ "@testing-library/jest-dom": "^7.0.1",
53
+ "@testing-library/react": "^16.3.3",
54
+ "@testing-library/user-event": "^14.6.8",
55
+ "@types/react": "^19.3.0",
56
+ "@types/react-dom": "^19.3.0",
57
+ "@vitejs/plugin-react": "^6.1.2",
58
+ "eslint": "^10.13.0",
59
+ "eslint-plugin-react-hooks": "^7.1.1",
60
+ "framer-motion": "^14.3.0",
61
+ "globals": "^17.13.0",
62
+ "jsdom": "^30.1.3",
63
+ "lucide-react": "^1.59.0",
64
+ "react": "^19.3.1",
65
+ "react-dom": "^19.3.1",
66
+ "tsup": "^9.0.0",
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.71.2",
69
+ "vite": "^8.3.4",
70
+ "vitest": "^5.0.5"
71
+ }
72
+ }