Skip to content

Commit 1c0c1dd

Browse files
authored
feat: implement flower menu component (codse#379)
1 parent 816539d commit 1c0c1dd

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

animata/list/flower-menu.stories.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
Codepen,
3+
Facebook,
4+
Github,
5+
Instagram,
6+
Linkedin,
7+
Twitch,
8+
Twitter,
9+
Youtube,
10+
} from "lucide-react";
11+
12+
import FlowerMenu from "@/animata/list/flower-menu";
13+
import { Meta, StoryObj } from "@storybook/react";
14+
15+
const meta = {
16+
title: "List/Flower Menu",
17+
component: FlowerMenu,
18+
parameters: {
19+
layout: "centered",
20+
},
21+
tags: ["autodocs"],
22+
argTypes: {},
23+
} satisfies Meta<typeof FlowerMenu>;
24+
25+
export default meta;
26+
type Story = StoryObj<typeof meta>;
27+
28+
export const Primary: Story = {
29+
args: {
30+
menuItems: [
31+
{ icon: Github, href: "https://github.com/" },
32+
{ icon: Twitter, href: "https://twitter.com/" },
33+
{ icon: Instagram, href: "https://instagram.com/" },
34+
{ icon: Linkedin, href: "https://www.linkedin.com/" },
35+
{ icon: Youtube, href: "https://www.youtube.com/" },
36+
{ icon: Twitch, href: "https://www.twitch.tv/" },
37+
{ icon: Facebook, href: "https://www.facebook.com/" },
38+
{ icon: Codepen, href: "https://www.codepen.io/" },
39+
],
40+
iconColor: "#ffffff",
41+
backgroundColor: "rgba(0, 0, 0)",
42+
animationDuration: 700,
43+
togglerSize: 40,
44+
},
45+
};

animata/list/flower-menu.tsx

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { useState } from "react";
2+
import Link from "next/link";
3+
4+
type MenuItem = {
5+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
6+
href: string;
7+
};
8+
9+
type FlowerMenuProps = {
10+
menuItems: MenuItem[];
11+
iconColor?: string;
12+
backgroundColor?: string;
13+
animationDuration?: number;
14+
togglerSize?: number;
15+
};
16+
17+
const MenuToggler = ({
18+
isOpen,
19+
onChange,
20+
backgroundColor,
21+
iconColor,
22+
animationDuration,
23+
togglerSize,
24+
iconSize,
25+
}: {
26+
isOpen: boolean;
27+
onChange: () => void;
28+
backgroundColor: string;
29+
iconColor: string;
30+
animationDuration: number;
31+
togglerSize: number;
32+
iconSize: number;
33+
}) => {
34+
const lineHeight = iconSize * 0.1;
35+
const lineWidth = iconSize * 0.8;
36+
const lineSpacing = iconSize * 0.25;
37+
38+
return (
39+
<>
40+
<input
41+
id="menu-toggler"
42+
type="checkbox"
43+
checked={isOpen}
44+
onChange={onChange}
45+
className="absolute inset-0 z-10 m-auto cursor-pointer opacity-0"
46+
style={{ width: togglerSize, height: togglerSize }}
47+
/>
48+
<label
49+
htmlFor="menu-toggler"
50+
className="absolute inset-0 z-20 m-auto flex cursor-pointer items-center justify-center rounded-full transition-all"
51+
style={{
52+
backgroundColor,
53+
color: iconColor,
54+
transitionDuration: `${animationDuration}ms`,
55+
width: togglerSize,
56+
height: togglerSize,
57+
}}
58+
>
59+
<span
60+
className="relative flex flex-col items-center justify-center"
61+
style={{ width: iconSize, height: iconSize }}
62+
>
63+
{[0, 1, 2].map((i) => (
64+
<span
65+
key={i}
66+
className={`absolute bg-current transition-all ${
67+
isOpen && i === 0
68+
? "opacity-0"
69+
: isOpen
70+
? `${i === 1 ? "rotate-45" : "-rotate-45"}`
71+
: ""
72+
}`}
73+
style={{
74+
transitionDuration: `${animationDuration}ms`,
75+
width: lineWidth,
76+
height: lineHeight,
77+
top: isOpen
78+
? `calc(50% - ${lineHeight / 2}px)`
79+
: `calc(50% + ${(i - 1) * lineSpacing}px - ${lineHeight / 2}px)`,
80+
}}
81+
/>
82+
))}
83+
</span>
84+
</label>
85+
</>
86+
);
87+
};
88+
89+
const MenuItem = ({
90+
item,
91+
index,
92+
isOpen,
93+
iconColor,
94+
backgroundColor,
95+
animationDuration,
96+
itemCount,
97+
itemSize,
98+
iconSize,
99+
}: {
100+
item: MenuItem;
101+
index: number;
102+
isOpen: boolean;
103+
iconColor: string;
104+
backgroundColor: string;
105+
animationDuration: number;
106+
itemCount: number;
107+
itemSize: number;
108+
iconSize: number;
109+
}) => {
110+
const Icon = item.icon;
111+
return (
112+
<li
113+
className={`absolute inset-0 m-auto transition-all ${isOpen ? "opacity-100" : "opacity-0"}`}
114+
style={{
115+
width: itemSize,
116+
height: itemSize,
117+
transform: isOpen
118+
? `rotate(${(360 / itemCount) * index}deg) translateX(-${itemSize + 30}px)`
119+
: "none",
120+
transitionDuration: `${animationDuration}ms`,
121+
}}
122+
>
123+
<Link
124+
href={item.href}
125+
target="_blank"
126+
rel="noopener noreferrer"
127+
className={`flex h-full w-full items-center justify-center rounded-full opacity-60 transition-all duration-100 ${
128+
isOpen ? "pointer-events-auto" : "pointer-events-none"
129+
} group hover:scale-125 hover:opacity-100`}
130+
style={{
131+
backgroundColor,
132+
color: iconColor,
133+
transform: `rotate(-${(360 / itemCount) * index}deg)`,
134+
transitionDuration: `${animationDuration}ms`,
135+
}}
136+
>
137+
<Icon
138+
className="transition-transform duration-200 group-hover:scale-125"
139+
style={{ width: iconSize, height: iconSize }}
140+
/>
141+
</Link>
142+
</li>
143+
);
144+
};
145+
146+
export default function FlowerMenu({
147+
menuItems,
148+
iconColor = "white",
149+
backgroundColor = "rgba(255, 255, 255, 0.2)",
150+
animationDuration = 500,
151+
togglerSize = 40,
152+
}: FlowerMenuProps) {
153+
const [isOpen, setIsOpen] = useState(false);
154+
const itemCount = menuItems.length;
155+
const itemSize = togglerSize * 2;
156+
const iconSize = Math.max(24, Math.floor(togglerSize * 0.6));
157+
158+
return (
159+
<nav className="relative min-h-64" style={{ width: togglerSize * 3, height: togglerSize * 3 }}>
160+
<MenuToggler
161+
isOpen={isOpen}
162+
onChange={() => setIsOpen(!isOpen)}
163+
backgroundColor={backgroundColor}
164+
iconColor={iconColor}
165+
animationDuration={animationDuration}
166+
togglerSize={togglerSize}
167+
iconSize={iconSize}
168+
/>
169+
<ul className="absolute inset-0 m-0 h-full w-full list-none p-0">
170+
{menuItems.map((item, index) => (
171+
<MenuItem
172+
key={index}
173+
item={item}
174+
index={index}
175+
isOpen={isOpen}
176+
iconColor={iconColor}
177+
backgroundColor={backgroundColor}
178+
animationDuration={animationDuration}
179+
itemCount={itemCount}
180+
itemSize={itemSize}
181+
iconSize={iconSize}
182+
/>
183+
))}
184+
</ul>
185+
</nav>
186+
);
187+
}

content/docs/list/flower-menu.mdx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Flower Menu
3+
description: A circular flower menu with several icons and a central close button.
4+
author: arjuncodess
5+
---
6+
7+
<ComponentPreview name="list-flower-menu--docs" />
8+
9+
## Installation
10+
11+
<Steps>
12+
13+
<Step>Run the following command</Step>
14+
15+
It will create a new file `flower-menu.tsx` inside the `components/animata/list` directory.
16+
17+
```bash
18+
mkdir -p components/animata/list && touch components/animata/list/flower-menu.tsx
19+
```
20+
21+
<Step>Paste the code</Step>
22+
23+
Open the newly created file and paste the following code:
24+
25+
```jsx file=<rootDir>/animata/list/flower-menu.tsx
26+
27+
```
28+
29+
</Steps>
30+
31+
## Credits
32+
33+
Built by [Arjun Vijay Prakash](https://github.com/arjuncodess).

0 commit comments

Comments
 (0)