Back to buttons
"use client";

import { cn } from "@/packages/components/react-shadcn/src/lib/utils";
import {
  AnimatePresence,
  LayoutGroup,
  motion,
  Transition,
  useReducedMotion,
} from "framer-motion";
import { Plus, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";

export interface MorphingButtonAction {
  /**
   * Display label for the action.
   */
  label: string;
  /**
   * Icon to display alongside the label.
   */
  icon: React.ReactNode;
  /**
   * Callback when action is clicked.
   */
  onClick: () => void;
}

export interface NativeMorphingButtonProps {
  /**
   * Array of actions to display in the expanded menu.
   */
  actions: MorphingButtonAction[];
  /**
   * Position of the FAB.
   * Default: 'bottom-right'
   */
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  /**
   * Whether to use fixed positioning.
   * Default: false (relative to container)
   */
  fixed?: boolean;
  /**
   * Custom icon when collapsed.
   */
  icon?: React.ReactNode;
  /**
   * Custom close icon when expanded.
   */
  closeIcon?: React.ReactNode;
  className?: string;
}

const positionClasses = {
  "bottom-right": "bottom-4 right-4",
  "bottom-left": "bottom-4 left-4",
  "top-right": "top-4 right-4",
  "top-left": "top-4 left-4",
};

const springTransition: Transition = {
  type: "spring",
  stiffness: 300,
  damping: 30,
};
const reducedTransition: Transition = { duration: 0.1 };

export function NativeMorphingButton({
  actions,
  position = "bottom-right",
  fixed = false,
  icon,
  closeIcon,
  className,
}: NativeMorphingButtonProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const shouldReduceMotion = useReducedMotion();
  const fabRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);
  const wasExpanded = useRef(false);

  const transition = shouldReduceMotion ? reducedTransition : springTransition;

  // Focus the first action on open, return focus to the FAB on close.
  useEffect(() => {
    if (isExpanded) {
      wasExpanded.current = true;
      menuRef.current
        ?.querySelector<HTMLButtonElement>('[role="menuitem"]')
        ?.focus();
    } else if (wasExpanded.current) {
      fabRef.current?.focus();
    }
  }, [isExpanded]);

  const handleMenuKeyDown = (e: React.KeyboardEvent) => {
    const items = Array.from(
      menuRef.current?.querySelectorAll<HTMLButtonElement>(
        '[role="menuitem"]'
      ) ?? []
    );
    if (!items.length) return;
    const currentIndex = items.indexOf(
      document.activeElement as HTMLButtonElement
    );
    if (e.key === "ArrowDown") {
      e.preventDefault();
      items[(currentIndex + 1) % items.length]?.focus();
    }
    if (e.key === "ArrowUp") {
      e.preventDefault();
      items[(currentIndex - 1 + items.length) % items.length]?.focus();
    }
  };

  return (
    <div
      className={cn(
        fixed ? "fixed" : "absolute",
        positionClasses[position],
        "z-50",
        className
      )}
      onKeyDown={(e) => {
        if (e.key === "Escape" && isExpanded) {
          setIsExpanded(false);
        }
      }}
    >
      <LayoutGroup>
        <motion.div
          layout
          className="relative"
          initial={false}
          animate={{
            width: isExpanded ? 280 : 56,
            height: isExpanded ? "auto" : 56,
            borderRadius: isExpanded ? 16 : 28,
          }}
          transition={transition}
        >
          {/* Main FAB Button */}
          <motion.button
            ref={fabRef}
            type="button"
            onClick={() => setIsExpanded(!isExpanded)}
            className="absolute right-0 bottom-0 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-shadow hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
            whileHover={shouldReduceMotion ? undefined : { scale: 1.05 }}
            whileTap={shouldReduceMotion ? undefined : { scale: 0.95 }}
            aria-label={isExpanded ? "Close menu" : "Open menu"}
            aria-expanded={isExpanded}
            aria-haspopup="menu"
          >
            <AnimatePresence mode="wait">
              {isExpanded ? (
                <motion.div
                  key="close"
                  aria-hidden="true"
                  initial={
                    shouldReduceMotion ? false : { rotate: -90, opacity: 0 }
                  }
                  animate={{ rotate: 0, opacity: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { rotate: 90, opacity: 0 }
                  }
                  transition={{ duration: 0.2 }}
                >
                  {closeIcon ?? <X className="h-5 w-5" />}
                </motion.div>
              ) : (
                <motion.div
                  key="open"
                  aria-hidden="true"
                  initial={
                    shouldReduceMotion ? false : { rotate: 90, opacity: 0 }
                  }
                  animate={{ rotate: 0, opacity: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { rotate: -90, opacity: 0 }
                  }
                  transition={{ duration: 0.2 }}
                >
                  {icon ?? <Plus className="h-5 w-5" />}
                </motion.div>
              )}
            </AnimatePresence>
          </motion.button>

          {/* Expanded Menu */}
          <AnimatePresence>
            {isExpanded && (
              <>
                <button
                  type="button"
                  aria-label="Close menu"
                  tabIndex={-1}
                  className="fixed inset-0 z-[-1] cursor-default bg-transparent"
                  onClick={() => setIsExpanded(false)}
                />
                <motion.div
                  ref={menuRef}
                  initial={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { opacity: 0, scale: 0.8 }
                  }
                  animate={{ opacity: 1, scale: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { opacity: 0, scale: 0.8 }
                  }
                  transition={{ duration: 0.2, delay: 0.1 }}
                  style={{ transformOrigin: "bottom right" }}
                  className="absolute bottom-0 right-0 w-64 rounded-2xl rounded-br-[28px] border border-border bg-card p-4 shadow-2xl"
                  role="menu"
                  onKeyDown={handleMenuKeyDown}
                >
                  <div className="mb-2 space-y-2">
                    {actions.map((action, index) => (
                      <motion.button
                        key={action.label}
                        type="button"
                        initial={
                          shouldReduceMotion ? false : { opacity: 0, x: -20 }
                        }
                        animate={{ opacity: 1, x: 0 }}
                        transition={{ delay: index * 0.05 + 0.2 }}
                        onClick={() => {
                          action.onClick();
                          setIsExpanded(false);
                        }}
                        className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                        role="menuitem"
                      >
                        <span aria-hidden="true" className="text-muted-foreground">
                          {action.icon}
                        </span>
                        <span className="font-medium">{action.label}</span>
                      </motion.button>
                    ))}
                  </div>
                </motion.div>
              </>
            )}
          </AnimatePresence>
        </motion.div>
      </LayoutGroup>
    </div>
  );
}

Native Morphing Button Demo

By Moumen Soliman

Try the interaction, copy the React source files, or download the complete example.

About Native Morphing Button Demo

Native Morphing Button Demo is an animated React and Tailwind CSS button by Moumen Soliman. Try the interaction, copy the React source, or download the example with creator credit.

Effects and categories
Hover effects, Animated
Using this example
Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.

Usage, Tailwind and licensing questions

Read the source files

packages/components/react-shadcn/src/components/native/demo/native-morphing-button-demo.tsx

"use client";

import { NativeMorphingButton } from "../native-morphing-button-shadcnui";
import { FileText, FolderPlus, Plus, Users } from "lucide-react";

export function NativeMorphingButtonDefault() {
  return (
    <div className="relative h-48 w-full">
      <NativeMorphingButton
        actions={[
          {
            label: "New Task",
            icon: <Plus className="h-4 w-4" />,
            onClick: () => {},
          },
          {
            label: "New Project",
            icon: <FolderPlus className="h-4 w-4" />,
            onClick: () => {},
          },
          {
            label: "New Team",
            icon: <Users className="h-4 w-4" />,
            onClick: () => {},
          },
        ]}
      />
    </div>
  );
}

export function NativeMorphingButtonCustomIcon() {
  return (
    <div className="relative h-48 w-full">
      <NativeMorphingButton
        icon={<FileText className="h-5 w-5" />}
        actions={[
          {
            label: "Draft",
            icon: <FileText className="h-4 w-4" />,
            onClick: () => {},
          },
          {
            label: "Template",
            icon: <FileText className="h-4 w-4" />,
            onClick: () => {},
          },
        ]}
      />
    </div>
  );
}

export function NativeMorphingButtonDemo() {
  return (
    <div className="flex flex-col gap-8 w-full">
      <div className="grid gap-6 md:grid-cols-2">
        <div className="space-y-2">
          <h3 className="font-semibold text-muted-foreground text-sm">
            Default
          </h3>
          <NativeMorphingButtonDefault />
        </div>
        <div className="space-y-2">
          <h3 className="font-semibold text-muted-foreground text-sm">
            Custom Icon
          </h3>
          <NativeMorphingButtonCustomIcon />
        </div>
      </div>
    </div>
  );
}

packages/components/react-shadcn/src/components/native/native-morphing-button-shadcnui.tsx

"use client";

import { cn } from "@/packages/components/react-shadcn/src/lib/utils";
import {
  AnimatePresence,
  LayoutGroup,
  motion,
  Transition,
  useReducedMotion,
} from "framer-motion";
import { Plus, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";

export interface MorphingButtonAction {
  /**
   * Display label for the action.
   */
  label: string;
  /**
   * Icon to display alongside the label.
   */
  icon: React.ReactNode;
  /**
   * Callback when action is clicked.
   */
  onClick: () => void;
}

export interface NativeMorphingButtonProps {
  /**
   * Array of actions to display in the expanded menu.
   */
  actions: MorphingButtonAction[];
  /**
   * Position of the FAB.
   * Default: 'bottom-right'
   */
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  /**
   * Whether to use fixed positioning.
   * Default: false (relative to container)
   */
  fixed?: boolean;
  /**
   * Custom icon when collapsed.
   */
  icon?: React.ReactNode;
  /**
   * Custom close icon when expanded.
   */
  closeIcon?: React.ReactNode;
  className?: string;
}

const positionClasses = {
  "bottom-right": "bottom-4 right-4",
  "bottom-left": "bottom-4 left-4",
  "top-right": "top-4 right-4",
  "top-left": "top-4 left-4",
};

const springTransition: Transition = {
  type: "spring",
  stiffness: 300,
  damping: 30,
};
const reducedTransition: Transition = { duration: 0.1 };

export function NativeMorphingButton({
  actions,
  position = "bottom-right",
  fixed = false,
  icon,
  closeIcon,
  className,
}: NativeMorphingButtonProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const shouldReduceMotion = useReducedMotion();
  const fabRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);
  const wasExpanded = useRef(false);

  const transition = shouldReduceMotion ? reducedTransition : springTransition;

  // Focus the first action on open, return focus to the FAB on close.
  useEffect(() => {
    if (isExpanded) {
      wasExpanded.current = true;
      menuRef.current
        ?.querySelector<HTMLButtonElement>('[role="menuitem"]')
        ?.focus();
    } else if (wasExpanded.current) {
      fabRef.current?.focus();
    }
  }, [isExpanded]);

  const handleMenuKeyDown = (e: React.KeyboardEvent) => {
    const items = Array.from(
      menuRef.current?.querySelectorAll<HTMLButtonElement>(
        '[role="menuitem"]'
      ) ?? []
    );
    if (!items.length) return;
    const currentIndex = items.indexOf(
      document.activeElement as HTMLButtonElement
    );
    if (e.key === "ArrowDown") {
      e.preventDefault();
      items[(currentIndex + 1) % items.length]?.focus();
    }
    if (e.key === "ArrowUp") {
      e.preventDefault();
      items[(currentIndex - 1 + items.length) % items.length]?.focus();
    }
  };

  return (
    <div
      className={cn(
        fixed ? "fixed" : "absolute",
        positionClasses[position],
        "z-50",
        className
      )}
      onKeyDown={(e) => {
        if (e.key === "Escape" && isExpanded) {
          setIsExpanded(false);
        }
      }}
    >
      <LayoutGroup>
        <motion.div
          layout
          className="relative"
          initial={false}
          animate={{
            width: isExpanded ? 280 : 56,
            height: isExpanded ? "auto" : 56,
            borderRadius: isExpanded ? 16 : 28,
          }}
          transition={transition}
        >
          {/* Main FAB Button */}
          <motion.button
            ref={fabRef}
            type="button"
            onClick={() => setIsExpanded(!isExpanded)}
            className="absolute right-0 bottom-0 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-shadow hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
            whileHover={shouldReduceMotion ? undefined : { scale: 1.05 }}
            whileTap={shouldReduceMotion ? undefined : { scale: 0.95 }}
            aria-label={isExpanded ? "Close menu" : "Open menu"}
            aria-expanded={isExpanded}
            aria-haspopup="menu"
          >
            <AnimatePresence mode="wait">
              {isExpanded ? (
                <motion.div
                  key="close"
                  aria-hidden="true"
                  initial={
                    shouldReduceMotion ? false : { rotate: -90, opacity: 0 }
                  }
                  animate={{ rotate: 0, opacity: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { rotate: 90, opacity: 0 }
                  }
                  transition={{ duration: 0.2 }}
                >
                  {closeIcon ?? <X className="h-5 w-5" />}
                </motion.div>
              ) : (
                <motion.div
                  key="open"
                  aria-hidden="true"
                  initial={
                    shouldReduceMotion ? false : { rotate: 90, opacity: 0 }
                  }
                  animate={{ rotate: 0, opacity: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { rotate: -90, opacity: 0 }
                  }
                  transition={{ duration: 0.2 }}
                >
                  {icon ?? <Plus className="h-5 w-5" />}
                </motion.div>
              )}
            </AnimatePresence>
          </motion.button>

          {/* Expanded Menu */}
          <AnimatePresence>
            {isExpanded && (
              <>
                <button
                  type="button"
                  aria-label="Close menu"
                  tabIndex={-1}
                  className="fixed inset-0 z-[-1] cursor-default bg-transparent"
                  onClick={() => setIsExpanded(false)}
                />
                <motion.div
                  ref={menuRef}
                  initial={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { opacity: 0, scale: 0.8 }
                  }
                  animate={{ opacity: 1, scale: 1 }}
                  exit={
                    shouldReduceMotion
                      ? { opacity: 0 }
                      : { opacity: 0, scale: 0.8 }
                  }
                  transition={{ duration: 0.2, delay: 0.1 }}
                  style={{ transformOrigin: "bottom right" }}
                  className="absolute bottom-0 right-0 w-64 rounded-2xl rounded-br-[28px] border border-border bg-card p-4 shadow-2xl"
                  role="menu"
                  onKeyDown={handleMenuKeyDown}
                >
                  <div className="mb-2 space-y-2">
                    {actions.map((action, index) => (
                      <motion.button
                        key={action.label}
                        type="button"
                        initial={
                          shouldReduceMotion ? false : { opacity: 0, x: -20 }
                        }
                        animate={{ opacity: 1, x: 0 }}
                        transition={{ delay: index * 0.05 + 0.2 }}
                        onClick={() => {
                          action.onClick();
                          setIsExpanded(false);
                        }}
                        className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                        role="menuitem"
                      >
                        <span aria-hidden="true" className="text-muted-foreground">
                          {action.icon}
                        </span>
                        <span className="font-medium">{action.label}</span>
                      </motion.button>
                    ))}
                  </div>
                </motion.div>
              </>
            )}
          </AnimatePresence>
        </motion.div>
      </LayoutGroup>
    </div>
  );
}

packages/components/react-shadcn/src/lib/utils.ts

import { cn as baseCn } from "@/packages/utils/src/cn";

export function cn(...inputs: any[]) {
  return baseCn(...inputs);
}

packages/utils/src/cn.ts

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

demo.tsx

export { NativeMorphingButtonDemo as default } from "@/packages/components/react-shadcn/src/components/native/demo/native-morphing-button-demo";

CSS

/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
      "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
      "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
      "Courier New", monospace;
    --spacing: 0.25rem;
    --text-sm: 0.875rem;
    --text-sm--line-height: calc(1.25 / 0.875);
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --radius-2xl: 1rem;
    --default-transition-duration: 150ms;
    --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring:where(:not(iframe)) {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  ::-webkit-calendar-picker-indicator {
    line-height: 1;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden="until-found"])) {
    display: none !important;
  }
}
@layer utilities {
  .absolute {
    position: absolute;
  }
  .fixed {
    position: fixed;
  }
  .relative {
    position: relative;
  }
  .inset-0 {
    inset: 0px;
  }
  .top-4 {
    top: calc(var(--spacing) * 4);
  }
  .right-0 {
    right: 0px;
  }
  .right-4 {
    right: calc(var(--spacing) * 4);
  }
  .bottom-0 {
    bottom: 0px;
  }
  .bottom-4 {
    bottom: calc(var(--spacing) * 4);
  }
  .left-4 {
    left: calc(var(--spacing) * 4);
  }
  .z-10 {
    z-index: 10;
  }
  .z-50 {
    z-index: 50;
  }
  .z-\[-1\] {
    z-index: -1;
  }
  .mb-2 {
    margin-bottom: calc(var(--spacing) * 2);
  }
  .flex {
    display: flex;
  }
  .grid {
    display: grid;
  }
  .h-4 {
    height: calc(var(--spacing) * 4);
  }
  .h-5 {
    height: calc(var(--spacing) * 5);
  }
  .h-14 {
    height: calc(var(--spacing) * 14);
  }
  .h-48 {
    height: calc(var(--spacing) * 48);
  }
  .w-4 {
    width: calc(var(--spacing) * 4);
  }
  .w-5 {
    width: calc(var(--spacing) * 5);
  }
  .w-14 {
    width: calc(var(--spacing) * 14);
  }
  .w-64 {
    width: calc(var(--spacing) * 64);
  }
  .w-full {
    width: 100%;
  }
  .cursor-default {
    cursor: default;
  }
  .flex-col {
    flex-direction: column;
  }
  .items-center {
    align-items: center;
  }
  .justify-center {
    justify-content: center;
  }
  .gap-3 {
    gap: calc(var(--spacing) * 3);
  }
  .gap-6 {
    gap: calc(var(--spacing) * 6);
  }
  .gap-8 {
    gap: calc(var(--spacing) * 8);
  }
  :where(.space-y-2 > :not(:last-child)) {
    --tw-space-y-reverse: 0;
    margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
    margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
  }
  .rounded-2xl {
    border-radius: var(--radius-2xl);
  }
  .rounded-full {
    border-radius: calc(infinity * 1px);
  }
  .rounded-lg {
    border-radius: var(--radius);
  }
  .rounded-br-\[28px\] {
    border-bottom-right-radius: 28px;
  }
  .border {
    border-style: var(--tw-border-style);
    border-width: 1px;
  }
  .border-border {
    border-color: var(--border);
  }
  .bg-primary {
    background-color: var(--primary);
  }
  .bg-transparent {
    background-color: transparent;
  }
  .p-4 {
    padding: calc(var(--spacing) * 4);
  }
  .px-4 {
    padding-inline: calc(var(--spacing) * 4);
  }
  .py-3 {
    padding-block: calc(var(--spacing) * 3);
  }
  .text-left {
    text-align: left;
  }
  .text-sm {
    font-size: var(--text-sm);
    line-height: var(--tw-leading, var(--text-sm--line-height));
  }
  .font-medium {
    --tw-font-weight: var(--font-weight-medium);
    font-weight: var(--font-weight-medium);
  }
  .font-semibold {
    --tw-font-weight: var(--font-weight-semibold);
    font-weight: var(--font-weight-semibold);
  }
  .text-muted-foreground {
    color: var(--muted-foreground);
  }
  .text-primary-foreground {
    color: var(--primary-foreground);
  }
  .shadow-2xl {
    --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .shadow-lg {
    --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .transition {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  .transition-colors {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  .transition-shadow {
    transition-property: box-shadow;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  @media (hover: hover) {
    .hover\:bg-muted:hover {
      background-color: var(--muted);
    }
    .hover\:shadow-xl:hover {
      --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
      box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
    }
  }
  .focus-visible\:ring-2:focus-visible {
    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .focus-visible\:ring-ring:focus-visible {
    --tw-ring-color: var(--ring);
  }
  .focus-visible\:ring-offset-2:focus-visible {
    --tw-ring-offset-width: 2px;
    --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  }
  .focus-visible\:outline-none:focus-visible {
    --tw-outline-style: none;
    outline-style: none;
  }
  @media (width >= 48rem) {
    .md\:grid-cols-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
}
:root {
  --background: #fff;
  --foreground: #111;
  --primary: #171717;
  --primary-foreground: #fff;
  --secondary: #eee;
  --secondary-foreground: #171717;
  --muted: #eee;
  --muted-foreground: #666;
  --accent: #eee;
  --accent-foreground: #171717;
  --border: #ddd;
  --input: #ddd;
  --ring: #777;
  --destructive: #dc2626;
  --destructive-foreground: #fff;
  --popover: #fff;
  --popover-foreground: #111;
  --success: #15803d;
  --success-foreground: #fff;
  --radius: .625rem;
  --hairline: 1px;
}
.dark {
  --background: #090909;
  --foreground: #fafafa;
  --primary: #fafafa;
  --primary-foreground: #171717;
  --secondary: #262626;
  --secondary-foreground: #fafafa;
  --muted: #262626;
  --muted-foreground: #a3a3a3;
  --accent: #262626;
  --accent-foreground: #fafafa;
  --border: #333;
  --input: #333;
  --popover: #171717;
  --popover-foreground: #fafafa;
}
@property --tw-space-y-reverse {
  syntax: "*";
  inherits: false;
  initial-value: 0;
}
@property --tw-border-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}
@property --tw-font-weight {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-inset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-offset-width {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --tw-ring-offset-color {
  syntax: "*";
  inherits: false;
  initial-value: #fff;
}
@property --tw-ring-offset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@layer properties {
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
    *, ::before, ::after, ::backdrop {
      --tw-space-y-reverse: 0;
      --tw-border-style: solid;
      --tw-font-weight: initial;
      --tw-shadow: 0 0 #0000;
      --tw-shadow-color: initial;
      --tw-shadow-alpha: 100%;
      --tw-inset-shadow: 0 0 #0000;
      --tw-inset-shadow-color: initial;
      --tw-inset-shadow-alpha: 100%;
      --tw-ring-color: initial;
      --tw-ring-shadow: 0 0 #0000;
      --tw-inset-ring-color: initial;
      --tw-inset-ring-shadow: 0 0 #0000;
      --tw-ring-inset: initial;
      --tw-ring-offset-width: 0px;
      --tw-ring-offset-color: #fff;
      --tw-ring-offset-shadow: 0 0 #0000;
    }
  }
}