From 87609c71d69221e1d2ffd0b824cdf6ff370c924b Mon Sep 17 00:00:00 2001 From: Radiumcoders Date: Thu, 2 Jul 2026 19:00:06 +0530 Subject: [PATCH 1/4] Align navbar control heights to 32px --- components/landing/landing-page.tsx | 4 ++-- components/theme-toggle.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/landing/landing-page.tsx b/components/landing/landing-page.tsx index a26e9a2..d3f5cc5 100644 --- a/components/landing/landing-page.tsx +++ b/components/landing/landing-page.tsx @@ -48,7 +48,7 @@ export function LandingPage() {
- - - - - {showcase.map((item) => ( - - {item.name} - - ))} - - +export function LandingPage() { + const router = useRouter(); + + return ( +
+ +
+
- Docs - +
+ {siteConfig.name} +

+ Evil
Buttons +

+
+

+ Animated buttons, built with an evil touch. +

+

+ A shadcn/ui registry of {showcase.length} interactive button + components. Live previews, copy-paste docs, one-command CLI + installs. +

+
+ + Browse Docs + + + GitHub + + router.push("/drop")} + className="w-full min-w-0 rounded-none font-doto text-sm font-black uppercase tracking-tight" + /> +
-
-
- - - -
+
+

+ {showcase.length} Components +

+

+ © {new Date().getFullYear()} {siteConfig.author.name} +

+
+ -
-

- {showcase.length} components -

-
- - Browse Docs - - - GitHub - +
+
+ {showcase.map((item) => ( + + ))}
-
+
); -} \ No newline at end of file +} diff --git a/components/landing/playground-page.tsx b/components/landing/playground-page.tsx new file mode 100644 index 0000000..262f6be --- /dev/null +++ b/components/landing/playground-page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { AppDialRoot } from "@/components/dial-root"; +import { FitToContainer } from "@/components/landing/fit-to-container"; +import { ButtonPreview } from "@/components/landing/previews"; +import { showcase } from "@/components/landing/showcase"; +import { ThemeSync } from "@/components/theme-sync"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { useAppTheme } from "@/hooks/use-app-theme"; +import { siteConfig } from "@/lib/seo"; +import { ArrowUpRight } from "@phosphor-icons/react"; +import "dialkit/styles.css"; +import Link from "next/link"; +import { useState } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function PlaygroundPage() { + const theme = useAppTheme(); + const [selected, setSelected] = useState(showcase[0].registryName); + const active = + showcase.find((item) => item.registryName === selected) ?? showcase[0]; + + return ( +
+ + +
+ + {siteConfig.name} + + Evil Buttons + + + +
+ + + + + Docs + + +
+
+ +
+ + + +
+ +
+

+ {showcase.length} components +

+
+ + Browse Docs + + + GitHub + +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/components/landing/previews/index.tsx b/components/landing/previews/index.tsx index f659df0..72c1f71 100644 --- a/components/landing/previews/index.tsx +++ b/components/landing/previews/index.tsx @@ -38,8 +38,39 @@ import { ThreeDButtonPreview, TrollButtonPreview, } from "./simple"; +import { + StaticRevealButtonPreview, + StaticHoldButtonPreview, + StaticHoldConfirmButtonPreview, + StaticSlideToDetonatePreview, + StaticDoubtButtonPreview, + StaticCaptchaButtonPreview, + StaticCooldownButtonPreview, + StaticMorphStatusButtonPreview, + StaticBrutalButtonPreview, + StaticDitherButtonPreview, + StaticGlitchButtonPreview, + StaticEvilEyeButtonPreview, + StaticAquaButtonPreview, + StaticFrameButtonPreview, + StaticHighlightButtonPreview, + StaticConfettiButtonPreview, + StaticCommandButtonPreview, + StaticCopyButtonPreview, + StaticClickPowerUpPreview, + StaticPillButtonPreview, + StaticDemonicButtonPreview, + StaticChromeButtonPreview, + StaticGridButtonPreview, + StaticMinimalButtonPreview, + StaticMoviePassButtonPreview, + StaticShinyButtonPreview, + StaticStickyButtonPreview, + StaticThreeDButtonPreview, + StaticTrollButtonPreview, +} from "./static"; -const buttonPreviews: Record = { +const dialButtonPreviews: Record = { "reveal-button": RevealButtonPreview, "command-button": CommandButtonPreview, "copy-button": CopyButtonPreview, @@ -71,8 +102,47 @@ const buttonPreviews: Record = { "hold-confirm-button": HoldConfirmButtonPreview, }; -export function ButtonPreview({ registryName }: { registryName: string }) { - const Preview = buttonPreviews[registryName]; +const staticButtonPreviews: Record = { + "reveal-button": StaticRevealButtonPreview, + "command-button": StaticCommandButtonPreview, + "copy-button": StaticCopyButtonPreview, + "click-powerup": StaticClickPowerUpPreview, + "dither-button": StaticDitherButtonPreview, + "hold-button": StaticHoldButtonPreview, + "demonic-button": StaticDemonicButtonPreview, + "evil-eye-button": StaticEvilEyeButtonPreview, + "aqua-button": StaticAquaButtonPreview, + "brutal-button": StaticBrutalButtonPreview, + "chrome-button": StaticChromeButtonPreview, + "frame-button": StaticFrameButtonPreview, + "glitch-button": StaticGlitchButtonPreview, + "grid-button": StaticGridButtonPreview, + "highlight-button": StaticHighlightButtonPreview, + minimal: StaticMinimalButtonPreview, + "movie-pass": StaticMoviePassButtonPreview, + "shiny-button": StaticShinyButtonPreview, + sticky: StaticStickyButtonPreview, + "3d-button": StaticThreeDButtonPreview, + "troll-button": StaticTrollButtonPreview, + "captcha-button": StaticCaptchaButtonPreview, + "doubt-button": StaticDoubtButtonPreview, + "slide-to-detonate": StaticSlideToDetonatePreview, + "morph-status-button": StaticMorphStatusButtonPreview, + "cooldown-button": StaticCooldownButtonPreview, + "pill-button": StaticPillButtonPreview, + "confetti-button": StaticConfettiButtonPreview, + "hold-confirm-button": StaticHoldConfirmButtonPreview, +}; + +export function ButtonPreview({ + registryName, + dial = false, +}: { + registryName: string; + dial?: boolean; +}) { + const map = dial ? dialButtonPreviews : staticButtonPreviews; + const Preview = map[registryName]; if (!Preview) return null; return ; } \ No newline at end of file diff --git a/components/landing/previews/static.tsx b/components/landing/previews/static.tsx new file mode 100644 index 0000000..a584663 --- /dev/null +++ b/components/landing/previews/static.tsx @@ -0,0 +1,320 @@ +"use client"; + +import { useIsDarkMode } from "@/hooks/use-app-theme"; +import { RevealButton } from "@/components/evil-buttons/reveal-button"; +import { HoldButton } from "@/components/evil-buttons/hold-button"; +import { HoldConfirmButton } from "@/components/evil-buttons/hold-confirm-button"; +import { SlideToDetonate } from "@/components/evil-buttons/slide-to-detonate"; +import { DoubtButton } from "@/components/evil-buttons/doubt-button"; +import { CaptchaButton } from "@/components/evil-buttons/captcha-button"; +import { CooldownButton } from "@/components/evil-buttons/cooldown-button"; +import { MorphStatusButton } from "@/components/evil-buttons/morph-status-button"; +import { BrutalButton } from "@/components/evil-buttons/brutal-button"; +import DitherButton from "@/components/evil-buttons/dither-button"; +import { GlitchButton } from "@/components/evil-buttons/glitch-button"; +import EvilEyeButton from "@/components/evil-buttons/evil-eye-button"; +import { AquaButton } from "@/components/evil-buttons/aqua-button"; +import { FrameButton } from "@/components/evil-buttons/frame-button"; +import { HighlightButton } from "@/components/evil-buttons/highlight-button"; +import { ConfettiButton } from "@/components/evil-buttons/confetti-button"; +import { CommandButton } from "@/components/evil-buttons/command-button"; +import { CopyButton } from "@/components/evil-buttons/copy-button"; +import { ClickPowerUp } from "@/components/evil-buttons/click-powerup"; +import { PillButton } from "@/components/evil-buttons/pill-button"; +import { DemonicButton } from "@/components/evil-buttons/demonic-button"; +import ChromeButton from "@/components/evil-buttons/chrome-button"; +import GridButton from "@/components/evil-buttons/grid-button"; +import MinimalButton from "@/components/evil-buttons/minimal"; +import MoviePassButton from "@/components/evil-buttons/movie-pass"; +import ShinyButton from "@/components/evil-buttons/shiny-button"; +import StickyButton from "@/components/evil-buttons/sticky"; +import { ThreeDButton } from "@/components/evil-buttons/3d-button"; +import TrollButton from "@/components/evil-buttons/troll-button"; +import { DeferredWebGLPreview } from "./shared"; +import { pillClassNames, themeColors } from "./theme"; + +export function StaticRevealButtonPreview() { + return ( + + ); +} + +export function StaticHoldButtonPreview() { + return ( + + ); +} + +export function StaticHoldConfirmButtonPreview() { + const isDark = useIsDarkMode(); + + return ( + + ); +} + +export function StaticSlideToDetonatePreview() { + return ( + + ); +} + +export function StaticDoubtButtonPreview() { + return ( + + ); +} + +export function StaticCaptchaButtonPreview() { + return ( + + ); +} + +export function StaticCooldownButtonPreview() { + return ( + + ); +} + +export function StaticMorphStatusButtonPreview() { + return ( + new Promise((resolve) => setTimeout(resolve, 1200))} + /> + ); +} + +export function StaticBrutalButtonPreview() { + const isDark = useIsDarkMode(); + const colors = isDark ? themeColors.dark : themeColors.light; + + return ( + + Click Me + + ); +} + +export function StaticDitherButtonPreview() { + const isDark = useIsDarkMode(); + + return ( + + Run It + + ); +} + +export function StaticGlitchButtonPreview() { + const isDark = useIsDarkMode(); + + return ( + + Launch + + ); +} + +export function StaticEvilEyeButtonPreview() { + return ( + + + Doom + + + ); +} + +export function StaticAquaButtonPreview() { + return Deploy Doom; +} + +export function StaticFrameButtonPreview() { + return ( + + Deploy + + ); +} + +export function StaticHighlightButtonPreview() { + const isDark = useIsDarkMode(); + const colors = isDark ? themeColors.dark : themeColors.light; + + return ( + + Send + + ); +} + +export function StaticConfettiButtonPreview() { + return ( + + ); +} + +export function StaticCommandButtonPreview() { + return ( + + Save + + ); +} + +export function StaticCopyButtonPreview() { + return ( + + ); +} + +export function StaticClickPowerUpPreview() { + return Doom; +} + +export function StaticPillButtonPreview() { + const isDark = useIsDarkMode(); + const classes = pillClassNames(isDark); + + return ( + + ); +} + +export function StaticDemonicButtonPreview() { + return ; +} + +export function StaticChromeButtonPreview() { + return ( + + Chromy + + ); +} + +export function StaticGridButtonPreview() { + return Click; +} + +export function StaticMinimalButtonPreview() { + return Apply; +} + +export function StaticMoviePassButtonPreview() { + return Deploy Doom; +} + +export function StaticShinyButtonPreview() { + return Search; +} + +export function StaticStickyButtonPreview() { + return Try to Click; +} + +export function StaticThreeDButtonPreview() { + return Continue; +} + +export function StaticTrollButtonPreview() { + return Click Me; +} \ No newline at end of file diff --git a/public/r/3d-button.json b/public/r/3d-button.json index b2ba6c9..cc61ad0 100644 --- a/public/r/3d-button.json +++ b/public/r/3d-button.json @@ -9,7 +9,7 @@ "path": "components/evil-buttons/3d-button.tsx", "type": "registry:ui", "target": "components/evil-buttons/3d-button.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, type HTMLMotionProps } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype ThreeDButtonProps = HTMLMotionProps<\"button\"> & {\n children?: React.ReactNode;\n};\n\nconst pressTransition = {\n type: \"spring\",\n stiffness: 620,\n damping: 32,\n mass: 0.5,\n} as const;\n\nexport function ThreeDButton({\n className,\n children = \"Continue\",\n ...props\n}: ThreeDButtonProps) {\n return (\n \n {/* Deck (bottom layer / shadow block) */}\n \n {/* Face (top layer / button surface) */}\n \n {/* Press spot (darkens on press) */}\n \n {/* Sheen / gloss stripe */}\n \n {/* Label */}\n \n {children}\n \n \n \n );\n}\n" + "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport { motion, type HTMLMotionProps } from \"motion/react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype ThreeDButtonProps = HTMLMotionProps<\"button\"> & {\r\n children?: React.ReactNode;\r\n};\r\n\r\nconst pressTransition = {\r\n type: \"spring\",\r\n stiffness: 620,\r\n damping: 32,\r\n mass: 0.5,\r\n} as const;\r\n\r\nexport function ThreeDButton({\r\n className,\r\n children = \"Continue\",\r\n ...props\r\n}: ThreeDButtonProps) {\r\n return (\r\n \r\n {/* Deck (bottom layer / shadow block) */}\r\n \r\n {/* Face (top layer / button surface) */}\r\n \r\n {/* Press spot (darkens on press) */}\r\n \r\n {/* Sheen / gloss stripe */}\r\n \r\n {/* Label */}\r\n \r\n {children}\r\n \r\n \r\n \r\n );\r\n}\r\n" } ], "dependencies": [ diff --git a/public/r/aqua-button.json b/public/r/aqua-button.json index 657d92f..4cbd774 100644 --- a/public/r/aqua-button.json +++ b/public/r/aqua-button.json @@ -9,7 +9,7 @@ "path": "components/evil-buttons/aqua-button.tsx", "type": "registry:ui", "target": "components/evil-buttons/aqua-button.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AquaButtonVariant = \"primary\" | \"secondary\";\n\nexport interface AquaButtonProps\n extends React.ButtonHTMLAttributes {\n variant?: AquaButtonVariant;\n}\n\nconst EASE = \"cubic-bezier(0.4, 0, 0.2, 1)\";\n\nconst VARIANTS = {\n primary: {\n text: \"text-[rgb(20,30,55)]\",\n textShadow: \"0 1px 0 rgba(255, 255, 255, 0.35)\",\n bg: \"linear-gradient(rgb(95, 160, 230), rgb(50, 115, 205) 55%, rgb(95, 160, 230))\",\n shadow:\n \"0 0.25em 0.375em rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(0, 30, 95, 0.55), inset 0 0.125em 0.25em rgba(0, 20, 80, 0.3)\",\n hoverExtra: \", 0 0 0.875em 0.0625em rgba(60, 150, 235, 0.55)\",\n focusExtra:\n \", 0 0 0 0.125em rgba(255, 255, 255, 0.95), 0 0 0 0.3125em rgba(40, 150, 255, 0.95), 0 0 1.25em 0.1875em rgba(60, 170, 255, 0.7)\",\n active:\n \"0 0.0625em 0.125em rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(0, 30, 95, 0.65), inset 0 0.25em 0.5em rgba(0, 20, 80, 0.55)\",\n },\n secondary: {\n text: \"text-[rgb(35,35,40)] dark:text-white\",\n textShadow: \"var(--aqua-text-shadow)\",\n bg: \"var(--aqua-bg)\",\n shadow: \"var(--aqua-shadow)\",\n hoverExtra: \"\",\n focusExtra: \"\",\n active: \"var(--aqua-shadow-active)\",\n },\n} as const;\n\nconst SECONDARY_THEME_CLASSES = [\n \"[--aqua-bg:linear-gradient(rgb(225,_226,_228),_rgb(245,_246,_248)_55%,_rgb(230,_230,_232))]\",\n \"[--aqua-shadow:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15)]\",\n \"[--aqua-shadow-hover:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15),0_0_0.875em_0.0625em_rgba(0,_0,_0,_0.18)]\",\n \"[--aqua-shadow-focus:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15),0_0_0_0.125em_rgba(255,_255,_255,_0.95),0_0_0_0.3125em_rgba(40,_150,_255,_0.95),0_0_1.25em_0.1875em_rgba(60,_170,_255,_0.55)]\",\n \"[--aqua-shadow-active:0_0.0625em_0.125em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.6),inset_0_0.25em_0.5em_rgba(0,_0,_0,_0.3)]\",\n \"[--aqua-text-shadow:0_1px_0_rgba(255,_255,_255,_0.7)]\",\n \"[--aqua-top-highlight:linear-gradient(rgba(255,_255,_255,_0.85),_rgba(255,_255,_255,_0.25))]\",\n \"[--aqua-bottom-highlight:linear-gradient(rgba(255,_255,_255,_0.15),_rgba(255,_255,_255,_0.5))]\",\n \"dark:[--aqua-bg:linear-gradient(rgb(40,_40,_45),_rgb(22,_22,_28)_55%,_rgb(40,_40,_45))]\",\n \"dark:[--aqua-shadow:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3)]\",\n \"dark:[--aqua-shadow-hover:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3),0_0_0.875em_0.0625em_rgba(100,_100,_255,_0.2)]\",\n \"dark:[--aqua-shadow-focus:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3),0_0_0_0.125em_rgba(255,_255,_255,_0.95),0_0_0_0.3125em_rgba(100,_120,_255,_0.8),0_0_1.25em_0.1875em_rgba(80,_100,_255,_0.5)]\",\n \"dark:[--aqua-shadow-active:0_0.0625em_0.125em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.1),inset_0_0.25em_0.5em_rgba(0,_0,_0,_0.5)]\",\n \"dark:[--aqua-text-shadow:0_1px_2px_rgba(0,_0,_0,_0.5)]\",\n \"dark:[--aqua-top-highlight:linear-gradient(rgba(255,_255,_255,_0.12),_rgba(255,_255,_255,_0.03))]\",\n \"dark:[--aqua-bottom-highlight:linear-gradient(rgba(255,_255,_255,_0.04),_rgba(255,_255,_255,_0.08))]\",\n] as const;\n\nexport const AquaButton = React.forwardRef(\n ({ className, variant = \"primary\", style, children, ...props }, ref) => {\n const v = VARIANTS[variant];\n const isSecondary = variant === \"secondary\";\n\n return (\n \n \n \n \n {children}\n \n \n );\n },\n);\n\nAquaButton.displayName = \"AquaButton\";\n\nexport default AquaButton;\n" + "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nexport type AquaButtonVariant = \"primary\" | \"secondary\";\r\n\r\nexport interface AquaButtonProps\r\n extends React.ButtonHTMLAttributes {\r\n variant?: AquaButtonVariant;\r\n}\r\n\r\nconst EASE = \"cubic-bezier(0.4, 0, 0.2, 1)\";\r\n\r\nconst VARIANTS = {\r\n primary: {\r\n text: \"text-[rgb(20,30,55)]\",\r\n textShadow: \"0 1px 0 rgba(255, 255, 255, 0.35)\",\r\n bg: \"linear-gradient(rgb(95, 160, 230), rgb(50, 115, 205) 55%, rgb(95, 160, 230))\",\r\n shadow:\r\n \"0 0.25em 0.375em rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(0, 30, 95, 0.55), inset 0 0.125em 0.25em rgba(0, 20, 80, 0.3)\",\r\n hoverExtra: \", 0 0 0.875em 0.0625em rgba(60, 150, 235, 0.55)\",\r\n focusExtra:\r\n \", 0 0 0 0.125em rgba(255, 255, 255, 0.95), 0 0 0 0.3125em rgba(40, 150, 255, 0.95), 0 0 1.25em 0.1875em rgba(60, 170, 255, 0.7)\",\r\n active:\r\n \"0 0.0625em 0.125em rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(0, 30, 95, 0.65), inset 0 0.25em 0.5em rgba(0, 20, 80, 0.55)\",\r\n },\r\n secondary: {\r\n text: \"text-[rgb(35,35,40)] dark:text-white\",\r\n textShadow: \"var(--aqua-text-shadow)\",\r\n bg: \"var(--aqua-bg)\",\r\n shadow: \"var(--aqua-shadow)\",\r\n hoverExtra: \"\",\r\n focusExtra: \"\",\r\n active: \"var(--aqua-shadow-active)\",\r\n },\r\n} as const;\r\n\r\nconst SECONDARY_THEME_CLASSES = [\r\n \"[--aqua-bg:linear-gradient(rgb(225,_226,_228),_rgb(245,_246,_248)_55%,_rgb(230,_230,_232))]\",\r\n \"[--aqua-shadow:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15)]\",\r\n \"[--aqua-shadow-hover:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15),0_0_0.875em_0.0625em_rgba(0,_0,_0,_0.18)]\",\r\n \"[--aqua-shadow-focus:0_0.25em_0.375em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.5),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.15),0_0_0_0.125em_rgba(255,_255,_255,_0.95),0_0_0_0.3125em_rgba(40,_150,_255,_0.95),0_0_1.25em_0.1875em_rgba(60,_170,_255,_0.55)]\",\r\n \"[--aqua-shadow-active:0_0.0625em_0.125em_rgba(0,_0,_0,_0.18),inset_0_0_0_1px_rgba(120,_122,_130,_0.6),inset_0_0.25em_0.5em_rgba(0,_0,_0,_0.3)]\",\r\n \"[--aqua-text-shadow:0_1px_0_rgba(255,_255,_255,_0.7)]\",\r\n \"[--aqua-top-highlight:linear-gradient(rgba(255,_255,_255,_0.85),_rgba(255,_255,_255,_0.25))]\",\r\n \"[--aqua-bottom-highlight:linear-gradient(rgba(255,_255,_255,_0.15),_rgba(255,_255,_255,_0.5))]\",\r\n \"dark:[--aqua-bg:linear-gradient(rgb(40,_40,_45),_rgb(22,_22,_28)_55%,_rgb(40,_40,_45))]\",\r\n \"dark:[--aqua-shadow:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3)]\",\r\n \"dark:[--aqua-shadow-hover:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3),0_0_0.875em_0.0625em_rgba(100,_100,_255,_0.2)]\",\r\n \"dark:[--aqua-shadow-focus:0_0.25em_0.375em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.08),inset_0_0.125em_0.25em_rgba(0,_0,_0,_0.3),0_0_0_0.125em_rgba(255,_255,_255,_0.95),0_0_0_0.3125em_rgba(100,_120,_255,_0.8),0_0_1.25em_0.1875em_rgba(80,_100,_255,_0.5)]\",\r\n \"dark:[--aqua-shadow-active:0_0.0625em_0.125em_rgba(0,_0,_0,_0.4),inset_0_0_0_1px_rgba(255,_255,_255,_0.1),inset_0_0.25em_0.5em_rgba(0,_0,_0,_0.5)]\",\r\n \"dark:[--aqua-text-shadow:0_1px_2px_rgba(0,_0,_0,_0.5)]\",\r\n \"dark:[--aqua-top-highlight:linear-gradient(rgba(255,_255,_255,_0.12),_rgba(255,_255,_255,_0.03))]\",\r\n \"dark:[--aqua-bottom-highlight:linear-gradient(rgba(255,_255,_255,_0.04),_rgba(255,_255,_255,_0.08))]\",\r\n] as const;\r\n\r\nexport const AquaButton = React.forwardRef(\r\n ({ className, variant = \"primary\", style, children, ...props }, ref) => {\r\n const v = VARIANTS[variant];\r\n const isSecondary = variant === \"secondary\";\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {children}\r\n \r\n \r\n );\r\n },\r\n);\r\n\r\nAquaButton.displayName = \"AquaButton\";\r\n\r\nexport default AquaButton;\r\n" } ], "dependencies": [ diff --git a/public/r/confetti-button.json b/public/r/confetti-button.json index 2d0f965..13a48af 100644 --- a/public/r/confetti-button.json +++ b/public/r/confetti-button.json @@ -9,7 +9,7 @@ "path": "components/evil-buttons/confetti-button.tsx", "type": "registry:ui", "target": "components/evil-buttons/confetti-button.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport confetti from \"canvas-confetti\";\nimport { motion, useAnimationControls } from \"motion/react\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface ConfettiButtonProps\n extends Omit, \"onClick\"> {\n /** Button label. Falls back to `label` when no children are provided. */\n children?: React.ReactNode;\n /** Label used when no children are provided. */\n label?: React.ReactNode;\n /** Confetti particles per burst. */\n particleCount?: number;\n /** Confetti spread in degrees. */\n spread?: number;\n /** Extra vertical launch velocity. */\n startVelocity?: number;\n /** Custom confetti colors. */\n colors?: string[];\n /** Fired after each confetti burst. */\n onCelebrate?: () => void;\n}\n\nfunction burstFromElement(\n element: HTMLElement,\n options: {\n particleCount: number;\n spread: number;\n startVelocity: number;\n colors?: string[];\n },\n) {\n const rect = element.getBoundingClientRect();\n const x = (rect.left + rect.width / 2) / window.innerWidth;\n const y = (rect.top + rect.height / 2) / window.innerHeight;\n\n void confetti({\n particleCount: options.particleCount,\n spread: options.spread,\n startVelocity: options.startVelocity,\n origin: { x, y },\n colors: options.colors,\n disableForReducedMotion: true,\n });\n}\n\nexport const ConfettiButton = React.forwardRef<\n HTMLButtonElement,\n ConfettiButtonProps\n>(\n (\n {\n children,\n label = \"Continue\",\n particleCount = 120,\n spread = 72,\n startVelocity = 38,\n colors,\n onCelebrate,\n className,\n disabled,\n variant,\n size,\n type = \"button\",\n ...props\n },\n ref,\n ) => {\n const buttonRef = React.useRef(null);\n const popControls = useAnimationControls();\n\n const setButtonRef = (node: HTMLButtonElement | null) => {\n buttonRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n };\n\n const handleClick = () => {\n if (disabled || !buttonRef.current) return;\n\n burstFromElement(buttonRef.current, {\n particleCount,\n spread,\n startVelocity,\n colors,\n });\n onCelebrate?.();\n\n void popControls\n .start({\n scale: 1.12,\n transition: {\n type: \"spring\",\n stiffness: 520,\n damping: 14,\n mass: 0.55,\n },\n })\n .then(() =>\n popControls.start({\n scale: 1,\n transition: {\n type: \"spring\",\n stiffness: 420,\n damping: 20,\n mass: 0.6,\n },\n }),\n );\n };\n\n const displayLabel = children ?? label;\n\n return (\n \n \n {displayLabel}\n \n \n );\n },\n);\n\nConfettiButton.displayName = \"ConfettiButton\";\n\nexport default ConfettiButton;" + "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport confetti from \"canvas-confetti\";\r\nimport { motion, useAnimationControls } from \"motion/react\";\r\nimport { Button } from \"@/components/ui/button\";\r\n\r\nexport interface ConfettiButtonProps\r\n extends Omit, \"onClick\"> {\r\n /** Button label. Falls back to `label` when no children are provided. */\r\n children?: React.ReactNode;\r\n /** Label used when no children are provided. */\r\n label?: React.ReactNode;\r\n /** Confetti particles per burst. */\r\n particleCount?: number;\r\n /** Confetti spread in degrees. */\r\n spread?: number;\r\n /** Extra vertical launch velocity. */\r\n startVelocity?: number;\r\n /** Custom confetti colors. */\r\n colors?: string[];\r\n /** Fired after each confetti burst. */\r\n onCelebrate?: () => void;\r\n}\r\n\r\nfunction burstFromElement(\r\n element: HTMLElement,\r\n options: {\r\n particleCount: number;\r\n spread: number;\r\n startVelocity: number;\r\n colors?: string[];\r\n },\r\n) {\r\n const rect = element.getBoundingClientRect();\r\n const x = (rect.left + rect.width / 2) / window.innerWidth;\r\n const y = (rect.top + rect.height / 2) / window.innerHeight;\r\n\r\n void confetti({\r\n particleCount: options.particleCount,\r\n spread: options.spread,\r\n startVelocity: options.startVelocity,\r\n origin: { x, y },\r\n colors: options.colors,\r\n disableForReducedMotion: true,\r\n });\r\n}\r\n\r\nexport const ConfettiButton = React.forwardRef<\r\n HTMLButtonElement,\r\n ConfettiButtonProps\r\n>(\r\n (\r\n {\r\n children,\r\n label = \"Continue\",\r\n particleCount = 120,\r\n spread = 72,\r\n startVelocity = 38,\r\n colors,\r\n onCelebrate,\r\n className,\r\n disabled,\r\n variant,\r\n size,\r\n type = \"button\",\r\n ...props\r\n },\r\n ref,\r\n ) => {\r\n const buttonRef = React.useRef(null);\r\n const popControls = useAnimationControls();\r\n\r\n const setButtonRef = (node: HTMLButtonElement | null) => {\r\n buttonRef.current = node;\r\n if (typeof ref === \"function\") ref(node);\r\n else if (ref) ref.current = node;\r\n };\r\n\r\n const handleClick = () => {\r\n if (disabled || !buttonRef.current) return;\r\n\r\n burstFromElement(buttonRef.current, {\r\n particleCount,\r\n spread,\r\n startVelocity,\r\n colors,\r\n });\r\n onCelebrate?.();\r\n\r\n void popControls\r\n .start({\r\n scale: 1.12,\r\n transition: {\r\n type: \"spring\",\r\n stiffness: 520,\r\n damping: 14,\r\n mass: 0.55,\r\n },\r\n })\r\n .then(() =>\r\n popControls.start({\r\n scale: 1,\r\n transition: {\r\n type: \"spring\",\r\n stiffness: 420,\r\n damping: 20,\r\n mass: 0.6,\r\n },\r\n }),\r\n );\r\n };\r\n\r\n const displayLabel = children ?? label;\r\n\r\n return (\r\n \r\n \r\n {displayLabel}\r\n \r\n \r\n );\r\n },\r\n);\r\n\r\nConfettiButton.displayName = \"ConfettiButton\";\r\n\r\nexport default ConfettiButton;" } ], "registryDependencies": [ diff --git a/public/r/hold-confirm-button.json b/public/r/hold-confirm-button.json index ff523cb..4ed4739 100644 --- a/public/r/hold-confirm-button.json +++ b/public/r/hold-confirm-button.json @@ -9,7 +9,7 @@ "path": "components/evil-buttons/hold-confirm-button.tsx", "type": "registry:ui", "target": "components/evil-buttons/hold-confirm-button.tsx", - "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport {\n animate,\n mapValue,\n motionValue,\n press,\n springValue,\n styleEffect,\n svgEffect,\n} from \"motion\";\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype HoldConfirmState = \"idle\" | \"holding\" | \"success\";\r\n\r\nexport interface HoldConfirmButtonProps\r\n extends Omit<\r\n React.ButtonHTMLAttributes,\r\n \"onClick\" | \"onAbort\"\r\n > {\r\n /** Milliseconds the user must hold before `onConfirm` fires. */\r\n duration?: number;\r\n /** Label shown while idle. */\r\n label?: React.ReactNode;\r\n /** Optional label shown while holding. Falls back to `label`. */\r\n holdingLabel?: React.ReactNode;\r\n /** Label shown after a successful hold. */\r\n successLabel?: React.ReactNode;\r\n /** Smallest scale reached at 100% progress. */\r\n minScale?: number;\r\n /** Diameter of the progress ring in pixels. */\r\n ringSize?: number;\r\n /** Stroke width of the progress ring. */\r\n ringStrokeWidth?: number;\r\n /** Color of the progress ring. */\r\n ringColor?: string;\r\n /** Fired when the hold reaches 100%. */\r\n onConfirm?: () => void;\r\n /** Fired when the hold is released early. Receives progress reached (0-1). */\r\n onAbort?: (progress: number) => void;\r\n /** Milliseconds to stay in the success state before resetting. Set to 0 to stay. */\r\n resetAfter?: number;\r\n}\r\n\r\nexport const HoldConfirmButton = React.forwardRef<\r\n HTMLButtonElement,\r\n HoldConfirmButtonProps\r\n>(\r\n (\r\n {\r\n duration = 2000,\r\n label = \"Hold to confirm\",\r\n holdingLabel,\r\n successLabel = \"Confirmed\",\r\n minScale = 0.9,\r\n ringSize = 280,\r\n ringStrokeWidth = 12,\n ringColor = \"#5eead4\",\n onConfirm,\r\n onAbort,\r\n resetAfter = 1600,\r\n className,\r\n disabled,\r\n type = \"button\",\r\n ...props\r\n },\r\n ref,\r\n ) => {\r\n const [state, setState] = React.useState(\"idle\");\n\n const buttonRef = React.useRef(null);\n const ringRef = React.useRef(null);\n const circleRef = React.useRef(null);\r\n const animationRef = React.useRef | null>(null);\r\n const stateRef = React.useRef(\"idle\");\r\n const resetTimeoutRef = React.useRef(null);\r\n\r\n const setButtonRef = (node: HTMLButtonElement | null) => {\r\n buttonRef.current = node;\r\n if (typeof ref === \"function\") ref(node);\r\n else if (ref) ref.current = node;\r\n };\r\n\r\n React.useEffect(() => {\r\n stateRef.current = state;\r\n }, [state]);\r\n\r\n React.useEffect(() => {\r\n const button = buttonRef.current;\n const ring = ringRef.current;\n const circle = circleRef.current;\n if (!button || !ring || !circle || disabled) return;\n\r\n const progress = motionValue(0);\n const scale = springValue(mapValue(progress, [0, 1], [1, minScale]), {\n stiffness: 420,\n damping: 28,\n mass: 0.6,\n });\n\n const ringOpacity = mapValue(progress, [0, 0.01, 1], [0, 1, 1]);\n\n const cancelStyle = styleEffect(button, { scale });\n const cancelRingOpacity = styleEffect(ring, { opacity: ringOpacity });\n const cancelSvg = svgEffect(circle, { pathLength: progress });\r\n\r\n const stopAnimation = () => {\r\n animationRef.current?.stop();\r\n animationRef.current = null;\r\n };\r\n\r\n const rewind = () => {\n stopAnimation();\n animate(progress, 0, { type: \"spring\", stiffness: 520, damping: 32 });\n };\n\r\n const complete = () => {\r\n stopAnimation();\r\n progress.set(1);\r\n setState(\"success\");\r\n onConfirm?.();\r\n\r\n if (resetAfter > 0) {\r\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n resetTimeoutRef.current = window.setTimeout(() => {\r\n rewind();\r\n setState(\"idle\");\r\n }, resetAfter);\r\n }\r\n };\r\n\r\n const cancelPress = press(button, () => {\r\n if (stateRef.current !== \"idle\") return;\r\n\r\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n\r\n setState(\"holding\");\r\n stopAnimation();\r\n progress.set(0);\r\n\r\n animationRef.current = animate(progress, 1, {\r\n duration: duration / 1000,\r\n ease: \"linear\",\r\n onComplete: complete,\r\n });\r\n\r\n return (_endEvent, { success }) => {\r\n if (stateRef.current === \"success\") return;\r\n\r\n if (!success || progress.get() < 1) {\r\n const reached = progress.get();\r\n setState(\"idle\");\r\n rewind();\r\n onAbort?.(reached);\r\n }\r\n };\r\n });\r\n\r\n return () => {\n cancelPress();\n cancelStyle();\n cancelRingOpacity();\n cancelSvg();\n stopAnimation();\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n };\r\n }, [disabled, duration, minScale, onAbort, onConfirm, resetAfter]);\r\n\r\n const radius = ringSize / 2 - ringStrokeWidth / 2 - 6;\n const center = ringSize / 2;\n const isHolding = state === \"holding\";\r\n const isSuccess = state === \"success\";\r\n const displayLabel = isSuccess\r\n ? successLabel\r\n : isHolding && holdingLabel !== undefined\r\n ? holdingLabel\r\n : label;\r\n\r\n return (\r\n \r\n \n \n \n\r\n \r\n {displayLabel}\r\n \r\n
\r\n );\r\n },\r\n);\r\n\r\nHoldConfirmButton.displayName = \"HoldConfirmButton\";\r\n\r\nexport default HoldConfirmButton;" + "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport {\r\n animate,\r\n mapValue,\r\n motionValue,\r\n press,\r\n springValue,\r\n styleEffect,\r\n svgEffect,\r\n} from \"motion\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype HoldConfirmState = \"idle\" | \"holding\" | \"success\";\r\n\r\nexport interface HoldConfirmButtonProps\r\n extends Omit<\r\n React.ButtonHTMLAttributes,\r\n \"onClick\" | \"onAbort\"\r\n > {\r\n /** Milliseconds the user must hold before `onConfirm` fires. */\r\n duration?: number;\r\n /** Label shown while idle. */\r\n label?: React.ReactNode;\r\n /** Optional label shown while holding. Falls back to `label`. */\r\n holdingLabel?: React.ReactNode;\r\n /** Label shown after a successful hold. */\r\n successLabel?: React.ReactNode;\r\n /** Smallest scale reached at 100% progress. */\r\n minScale?: number;\r\n /** Diameter of the progress ring in pixels. */\r\n ringSize?: number;\r\n /** Stroke width of the progress ring. */\r\n ringStrokeWidth?: number;\r\n /** Color of the progress ring. */\r\n ringColor?: string;\r\n /** Fired when the hold reaches 100%. */\r\n onConfirm?: () => void;\r\n /** Fired when the hold is released early. Receives progress reached (0-1). */\r\n onAbort?: (progress: number) => void;\r\n /** Milliseconds to stay in the success state before resetting. Set to 0 to stay. */\r\n resetAfter?: number;\r\n}\r\n\r\nexport const HoldConfirmButton = React.forwardRef<\r\n HTMLButtonElement,\r\n HoldConfirmButtonProps\r\n>(\r\n (\r\n {\r\n duration = 2000,\r\n label = \"Hold to confirm\",\r\n holdingLabel,\r\n successLabel = \"Confirmed\",\r\n minScale = 0.9,\r\n ringSize = 280,\r\n ringStrokeWidth = 12,\r\n ringColor = \"#5eead4\",\r\n onConfirm,\r\n onAbort,\r\n resetAfter = 1600,\r\n className,\r\n disabled,\r\n type = \"button\",\r\n ...props\r\n },\r\n ref,\r\n ) => {\r\n const [state, setState] = React.useState(\"idle\");\r\n\r\n const buttonRef = React.useRef(null);\r\n const ringRef = React.useRef(null);\r\n const circleRef = React.useRef(null);\r\n const animationRef = React.useRef | null>(null);\r\n const stateRef = React.useRef(\"idle\");\r\n const resetTimeoutRef = React.useRef(null);\r\n\r\n const setButtonRef = (node: HTMLButtonElement | null) => {\r\n buttonRef.current = node;\r\n if (typeof ref === \"function\") ref(node);\r\n else if (ref) ref.current = node;\r\n };\r\n\r\n React.useEffect(() => {\r\n stateRef.current = state;\r\n }, [state]);\r\n\r\n React.useEffect(() => {\r\n const button = buttonRef.current;\r\n const ring = ringRef.current;\r\n const circle = circleRef.current;\r\n if (!button || !ring || !circle || disabled) return;\r\n\r\n const progress = motionValue(0);\r\n const scale = springValue(mapValue(progress, [0, 1], [1, minScale]), {\r\n stiffness: 420,\r\n damping: 28,\r\n mass: 0.6,\r\n });\r\n\r\n const ringOpacity = mapValue(progress, [0, 0.01, 1], [0, 1, 1]);\r\n\r\n const cancelStyle = styleEffect(button, { scale });\r\n const cancelRingOpacity = styleEffect(ring, { opacity: ringOpacity });\r\n const cancelSvg = svgEffect(circle, { pathLength: progress });\r\n\r\n const stopAnimation = () => {\r\n animationRef.current?.stop();\r\n animationRef.current = null;\r\n };\r\n\r\n const rewind = () => {\r\n stopAnimation();\r\n animate(progress, 0, { type: \"spring\", stiffness: 520, damping: 32 });\r\n };\r\n\r\n const complete = () => {\r\n stopAnimation();\r\n progress.set(1);\r\n setState(\"success\");\r\n onConfirm?.();\r\n\r\n if (resetAfter > 0) {\r\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n resetTimeoutRef.current = window.setTimeout(() => {\r\n rewind();\r\n setState(\"idle\");\r\n }, resetAfter);\r\n }\r\n };\r\n\r\n const cancelPress = press(button, () => {\r\n if (stateRef.current !== \"idle\") return;\r\n\r\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n\r\n setState(\"holding\");\r\n stopAnimation();\r\n progress.set(0);\r\n\r\n animationRef.current = animate(progress, 1, {\r\n duration: duration / 1000,\r\n ease: \"linear\",\r\n onComplete: complete,\r\n });\r\n\r\n return (_endEvent, { success }) => {\r\n if (stateRef.current === \"success\") return;\r\n\r\n if (!success || progress.get() < 1) {\r\n const reached = progress.get();\r\n setState(\"idle\");\r\n rewind();\r\n onAbort?.(reached);\r\n }\r\n };\r\n });\r\n\r\n return () => {\r\n cancelPress();\r\n cancelStyle();\r\n cancelRingOpacity();\r\n cancelSvg();\r\n stopAnimation();\r\n if (resetTimeoutRef.current !== null) {\r\n window.clearTimeout(resetTimeoutRef.current);\r\n }\r\n };\r\n }, [disabled, duration, minScale, onAbort, onConfirm, resetAfter]);\r\n\r\n const radius = ringSize / 2 - ringStrokeWidth / 2 - 6;\r\n const center = ringSize / 2;\r\n const isHolding = state === \"holding\";\r\n const isSuccess = state === \"success\";\r\n const displayLabel = isSuccess\r\n ? successLabel\r\n : isHolding && holdingLabel !== undefined\r\n ? holdingLabel\r\n : label;\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n\r\n \r\n {displayLabel}\r\n \r\n \r\n );\r\n },\r\n);\r\n\r\nHoldConfirmButton.displayName = \"HoldConfirmButton\";\r\n\r\nexport default HoldConfirmButton;" } ], "dependencies": [ diff --git a/public/r/index.json b/public/r/index.json index 25cb702..28bee77 100644 --- a/public/r/index.json +++ b/public/r/index.json @@ -253,7 +253,7 @@ "description": "A plain shadcn button that pops on click and fires a confetti celebration burst.", "files": [ "components/evil-buttons/confetti-button.tsx" - ] + ] }, { "name": "hold-confirm-button", diff --git a/public/r/pill-button.json b/public/r/pill-button.json index f6dbe1b..7718ee0 100644 --- a/public/r/pill-button.json +++ b/public/r/pill-button.json @@ -9,7 +9,7 @@ "path": "components/evil-buttons/pill-button.tsx", "type": "registry:ui", "target": "components/evil-buttons/pill-button.tsx", - "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype RollingLabelProps = {\n label: string\n}\n\nfunction RollingLabel({ label }: RollingLabelProps) {\n return (\n \n \n {label}\n \n {label}\n \n \n \n )\n}\n\ntype PillFaceProps = {\n label: string\n className?: string\n}\n\nfunction PillFace({ label, className }: PillFaceProps) {\n return (\n \n \n \n )\n}\n\ntype PillButtonProps = {\n primaryLabel: string\n secondaryLabel: string\n primaryClassName?: string\n secondaryClassName?: string\n isOpen?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n className?: string\n ariaLabel?: string | ((isOpen: boolean) => string)\n}\n\nfunction PillButton({\n primaryLabel,\n secondaryLabel,\n primaryClassName,\n secondaryClassName,\n isOpen: isOpenProp,\n defaultOpen = false,\n onOpenChange,\n className,\n ariaLabel,\n}: PillButtonProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen)\n const isControlled = isOpenProp !== undefined\n const isOpen = isControlled ? isOpenProp : internalOpen\n\n const setOpen = (next: boolean) => {\n if (!isControlled) {\n setInternalOpen(next)\n }\n onOpenChange?.(next)\n }\n\n const toggle = () => setOpen(!isOpen)\n\n const resolvedAriaLabel =\n typeof ariaLabel === \"function\"\n ? ariaLabel(isOpen)\n : ariaLabel ?? (isOpen ? secondaryLabel : primaryLabel)\n\n return (\n {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n toggle()\n }\n }}\n className={cn(\n \"relative h-10 w-20 cursor-pointer overflow-hidden rounded-full\",\n className\n )}\n aria-expanded={isOpen}\n aria-label={resolvedAriaLabel}\n >\n \n \n \n \n \n )\n}\n\nexport { PillButton, PillFace, RollingLabel }\nexport type { PillButtonProps, PillFaceProps, RollingLabelProps }" + "content": "\"use client\"\r\n\r\nimport { useState } from \"react\"\r\nimport { motion } from \"motion/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\ntype RollingLabelProps = {\r\n label: string\r\n}\r\n\r\nfunction RollingLabel({ label }: RollingLabelProps) {\r\n return (\r\n \r\n \r\n {label}\r\n \r\n {label}\r\n \r\n \r\n \r\n )\r\n}\r\n\r\ntype PillFaceProps = {\r\n label: string\r\n className?: string\r\n}\r\n\r\nfunction PillFace({ label, className }: PillFaceProps) {\r\n return (\r\n \r\n \r\n \r\n )\r\n}\r\n\r\ntype PillButtonProps = {\r\n primaryLabel: string\r\n secondaryLabel: string\r\n primaryClassName?: string\r\n secondaryClassName?: string\r\n isOpen?: boolean\r\n defaultOpen?: boolean\r\n onOpenChange?: (open: boolean) => void\r\n className?: string\r\n ariaLabel?: string | ((isOpen: boolean) => string)\r\n}\r\n\r\nfunction PillButton({\r\n primaryLabel,\r\n secondaryLabel,\r\n primaryClassName,\r\n secondaryClassName,\r\n isOpen: isOpenProp,\r\n defaultOpen = false,\r\n onOpenChange,\r\n className,\r\n ariaLabel,\r\n}: PillButtonProps) {\r\n const [internalOpen, setInternalOpen] = useState(defaultOpen)\r\n const isControlled = isOpenProp !== undefined\r\n const isOpen = isControlled ? isOpenProp : internalOpen\r\n\r\n const setOpen = (next: boolean) => {\r\n if (!isControlled) {\r\n setInternalOpen(next)\r\n }\r\n onOpenChange?.(next)\r\n }\r\n\r\n const toggle = () => setOpen(!isOpen)\r\n\r\n const resolvedAriaLabel =\r\n typeof ariaLabel === \"function\"\r\n ? ariaLabel(isOpen)\r\n : ariaLabel ?? (isOpen ? secondaryLabel : primaryLabel)\r\n\r\n return (\r\n {\r\n if (e.key === \"Enter\" || e.key === \" \") {\r\n e.preventDefault()\r\n toggle()\r\n }\r\n }}\r\n className={cn(\r\n \"relative h-10 w-20 cursor-pointer overflow-hidden rounded-full\",\r\n className\r\n )}\r\n aria-expanded={isOpen}\r\n aria-label={resolvedAriaLabel}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n )\r\n}\r\n\r\nexport { PillButton, PillFace, RollingLabel }\r\nexport type { PillButtonProps, PillFaceProps, RollingLabelProps }" } ], "dependencies": [