"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
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;
}
}
}