"use client";
import * as React from "react";
import {
AnimatePresence,
animate,
motion,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
useVelocity,
type AnimationPlaybackControls,
} from "motion/react";
import { cn } from "@/lib/utils";
export interface TrollButtonProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart"
> {
/** Label used when no children are provided. */
label?: React.ReactNode;
/** Label shown once the button gives up and lets itself be clicked. */
surrenderLabel?: React.ReactNode;
/** Milliseconds of chasing before the button gets tired and gives up. */
giveUpAfter?: number;
/** How close in px the cursor can get to the button's edge before it dodges. */
fleeRadius?: number;
/** Maximum distance in px the button strays from its resting spot. */
range?: number;
/** Milliseconds after a successful click before the button starts fleeing again. Set to 0 to stay caught. */
resetAfter?: number;
}
type TrollState = "fleeing" | "tired";
const DODGE_SPRING = { stiffness: 520, damping: 30, mass: 0.6 };
const labelVariants = {
enter: { y: 8, opacity: 0 },
center: { y: 0, opacity: 1 },
exit: { y: -8, opacity: 0 },
};
const TrollButton = React.forwardRef<HTMLButtonElement, TrollButtonProps>(
(
{
children,
label = "Click Me",
surrenderLabel = "Fine, click me",
giveUpAfter = 3500,
fleeRadius = 56,
range = 140,
resetAfter = 1500,
onClick,
onPointerDown,
className,
disabled,
type = "button",
...props
},
ref,
) => {
const wrapperRef = React.useRef<HTMLSpanElement>(null);
const offsetRef = React.useRef({ x: 0, y: 0 });
const staminaRef = React.useRef<AnimationPlaybackControls | null>(null);
const resetTimerRef = React.useRef<number | undefined>(undefined);
const [state, setState] = React.useState<TrollState>("fleeing");
const reduceMotion = useReducedMotion();
const evasive = state === "fleeing" && !reduceMotion && !disabled;
const targetX = useMotionValue(0);
const targetY = useMotionValue(0);
const x = useSpring(targetX, DODGE_SPRING);
const y = useSpring(targetY, DODGE_SPRING);
// Lean into each dodge based on how fast the button is moving sideways.
const velocityX = useVelocity(x);
const rotate = useTransform(velocityX, [-2400, 0, 2400], [-10, 0, 10]);
const stamina = useMotionValue(1);
const staminaOpacity = useTransform(stamina, (value) => (value < 1 ? 1 : 0));
const moveTo = React.useCallback(
(nextX: number, nextY: number) => {
offsetRef.current = { x: nextX, y: nextY };
targetX.set(nextX);
targetY.set(nextY);
},
[targetX, targetY],
);
const startTiring = React.useCallback(() => {
if (staminaRef.current) return;
staminaRef.current = animate(stamina, 0, {
duration: giveUpAfter / 1000,
ease: "linear",
onComplete: () => {
moveTo(0, 0);
setState("tired");
},
});
}, [giveUpAfter, moveTo, stamina]);
const dodge = React.useCallback(
(pointerX: number, pointerY: number, force = false) => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
// Measure from the resting slot, then add the current offset to find the button.
const rest = wrapper.getBoundingClientRect();
const { x: offsetX, y: offsetY } = offsetRef.current;
const centerX = rest.left + rest.width / 2 + offsetX;
const centerY = rest.top + rest.height / 2 + offsetY;
const dx = centerX - pointerX;
const dy = centerY - pointerY;
const outsideX = Math.max(Math.abs(dx) - rest.width / 2, 0);
const outsideY = Math.max(Math.abs(dy) - rest.height / 2, 0);
const gap = Math.hypot(outsideX, outsideY);
if (!force && gap >= fleeRadius) return;
const length = Math.hypot(dx, dy) || 1;
const push = fleeRadius - gap + 24;
let nextX = offsetX + (dx / length) * push;
let nextY = offsetY + (dy / length) * push;
const rangeY = range * 0.6;
// Cornered: slip past the cursor to the opposite side of the slot.
if (Math.abs(nextX) > range || Math.abs(nextY) > rangeY) {
nextX = -Math.sign(offsetX || dx || 1) * range * 0.7;
nextY = -offsetY * 0.5;
}
moveTo(nextX, nextY);
startTiring();
},
[fleeRadius, moveTo, range, startTiring],
);
React.useEffect(() => {
if (!evasive) return;
const onPointerMove = (event: PointerEvent) => {
if (event.pointerType === "touch") return;
dodge(event.clientX, event.clientY);
};
window.addEventListener("pointermove", onPointerMove, { passive: true });
return () => window.removeEventListener("pointermove", onPointerMove);
}, [dodge, evasive]);
React.useEffect(
() => () => {
staminaRef.current?.stop();
window.clearTimeout(resetTimerRef.current);
},
[],
);
const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
onPointerDown?.(event);
// Touch has no hover to flee from, so tapping triggers a dodge instead.
if (evasive && event.pointerType === "touch") {
dodge(event.clientX, event.clientY, true);
}
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Keyboard activation (detail 0) always goes through.
if (evasive && event.detail !== 0) {
event.preventDefault();
return;
}
onClick?.(event);
if (state === "tired" && resetAfter > 0) {
window.clearTimeout(resetTimerRef.current);
resetTimerRef.current = window.setTimeout(() => {
staminaRef.current = null;
stamina.set(1);
setState("fleeing");
}, resetAfter);
}
};
const idleLabel = children ?? label;
const currentLabel = state === "tired" ? surrenderLabel : idleLabel;
return (
<span ref={wrapperRef} className="relative inline-flex">
<motion.button
ref={ref}
type={type}
disabled={disabled}
style={{ x, y, rotate }}
onPointerDown={handlePointerDown}
onClick={handleClick}
animate={
state === "tired" && !reduceMotion
? { scaleY: [1, 0.92, 1.03, 1], scaleX: [1, 1.05, 0.98, 1] }
: undefined
}
transition={{ duration: 0.45, ease: "easeOut" }}
className={cn(
"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
// Motion owns `transform` for the dodge, lean, and squash, so the press animates the separate `scale` property.
"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
// Press eases in fast and settles the shadow; release springs back on the slower base curve.
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<span className="relative inline-grid">
{/* Invisible copies reserve the wider label so the button never resizes. */}
<span
aria-hidden
className="invisible col-start-1 row-start-1 whitespace-nowrap"
>
{idleLabel}
</span>
<span
aria-hidden
className="invisible col-start-1 row-start-1 whitespace-nowrap"
>
{surrenderLabel}
</span>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
key={state}
variants={labelVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ type: "spring", stiffness: 420, damping: 30 }}
className="col-start-1 row-start-1 whitespace-nowrap text-center"
>
{currentLabel}
</motion.span>
</AnimatePresence>
</span>
{/* Stamina drains while the button is being chased. */}
<motion.span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-left bg-neutral-50/35"
style={{
scaleX: stamina,
opacity: state === "fleeing" ? staminaOpacity : 0,
}}
/>
</motion.button>
</span>
);
},
);
TrollButton.displayName = "TrollButton";
export { TrollButton };
export default TrollButton;
TrollButton
By jay sharma
Try the interaction, copy the React source files, or download the complete example.
About TrollButton
TrollButton is an animated React and Tailwind CSS button by jay sharma. 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
import-examples/troll-button.tsx
import TrollButton from "@/components/evil-buttons/troll-button";
export function ButtonDemo() {
return (
<TrollButton giveUpAfter={3000} onClick={() => console.log("Caught it")}>
Click If You Can
</TrollButton>
);
}
components/evil-buttons/troll-button.tsx
"use client";
import * as React from "react";
import {
AnimatePresence,
animate,
motion,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
useVelocity,
type AnimationPlaybackControls,
} from "motion/react";
import { cn } from "@/lib/utils";
export interface TrollButtonProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart"
> {
/** Label used when no children are provided. */
label?: React.ReactNode;
/** Label shown once the button gives up and lets itself be clicked. */
surrenderLabel?: React.ReactNode;
/** Milliseconds of chasing before the button gets tired and gives up. */
giveUpAfter?: number;
/** How close in px the cursor can get to the button's edge before it dodges. */
fleeRadius?: number;
/** Maximum distance in px the button strays from its resting spot. */
range?: number;
/** Milliseconds after a successful click before the button starts fleeing again. Set to 0 to stay caught. */
resetAfter?: number;
}
type TrollState = "fleeing" | "tired";
const DODGE_SPRING = { stiffness: 520, damping: 30, mass: 0.6 };
const labelVariants = {
enter: { y: 8, opacity: 0 },
center: { y: 0, opacity: 1 },
exit: { y: -8, opacity: 0 },
};
const TrollButton = React.forwardRef<HTMLButtonElement, TrollButtonProps>(
(
{
children,
label = "Click Me",
surrenderLabel = "Fine, click me",
giveUpAfter = 3500,
fleeRadius = 56,
range = 140,
resetAfter = 1500,
onClick,
onPointerDown,
className,
disabled,
type = "button",
...props
},
ref,
) => {
const wrapperRef = React.useRef<HTMLSpanElement>(null);
const offsetRef = React.useRef({ x: 0, y: 0 });
const staminaRef = React.useRef<AnimationPlaybackControls | null>(null);
const resetTimerRef = React.useRef<number | undefined>(undefined);
const [state, setState] = React.useState<TrollState>("fleeing");
const reduceMotion = useReducedMotion();
const evasive = state === "fleeing" && !reduceMotion && !disabled;
const targetX = useMotionValue(0);
const targetY = useMotionValue(0);
const x = useSpring(targetX, DODGE_SPRING);
const y = useSpring(targetY, DODGE_SPRING);
// Lean into each dodge based on how fast the button is moving sideways.
const velocityX = useVelocity(x);
const rotate = useTransform(velocityX, [-2400, 0, 2400], [-10, 0, 10]);
const stamina = useMotionValue(1);
const staminaOpacity = useTransform(stamina, (value) => (value < 1 ? 1 : 0));
const moveTo = React.useCallback(
(nextX: number, nextY: number) => {
offsetRef.current = { x: nextX, y: nextY };
targetX.set(nextX);
targetY.set(nextY);
},
[targetX, targetY],
);
const startTiring = React.useCallback(() => {
if (staminaRef.current) return;
staminaRef.current = animate(stamina, 0, {
duration: giveUpAfter / 1000,
ease: "linear",
onComplete: () => {
moveTo(0, 0);
setState("tired");
},
});
}, [giveUpAfter, moveTo, stamina]);
const dodge = React.useCallback(
(pointerX: number, pointerY: number, force = false) => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
// Measure from the resting slot, then add the current offset to find the button.
const rest = wrapper.getBoundingClientRect();
const { x: offsetX, y: offsetY } = offsetRef.current;
const centerX = rest.left + rest.width / 2 + offsetX;
const centerY = rest.top + rest.height / 2 + offsetY;
const dx = centerX - pointerX;
const dy = centerY - pointerY;
const outsideX = Math.max(Math.abs(dx) - rest.width / 2, 0);
const outsideY = Math.max(Math.abs(dy) - rest.height / 2, 0);
const gap = Math.hypot(outsideX, outsideY);
if (!force && gap >= fleeRadius) return;
const length = Math.hypot(dx, dy) || 1;
const push = fleeRadius - gap + 24;
let nextX = offsetX + (dx / length) * push;
let nextY = offsetY + (dy / length) * push;
const rangeY = range * 0.6;
// Cornered: slip past the cursor to the opposite side of the slot.
if (Math.abs(nextX) > range || Math.abs(nextY) > rangeY) {
nextX = -Math.sign(offsetX || dx || 1) * range * 0.7;
nextY = -offsetY * 0.5;
}
moveTo(nextX, nextY);
startTiring();
},
[fleeRadius, moveTo, range, startTiring],
);
React.useEffect(() => {
if (!evasive) return;
const onPointerMove = (event: PointerEvent) => {
if (event.pointerType === "touch") return;
dodge(event.clientX, event.clientY);
};
window.addEventListener("pointermove", onPointerMove, { passive: true });
return () => window.removeEventListener("pointermove", onPointerMove);
}, [dodge, evasive]);
React.useEffect(
() => () => {
staminaRef.current?.stop();
window.clearTimeout(resetTimerRef.current);
},
[],
);
const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
onPointerDown?.(event);
// Touch has no hover to flee from, so tapping triggers a dodge instead.
if (evasive && event.pointerType === "touch") {
dodge(event.clientX, event.clientY, true);
}
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Keyboard activation (detail 0) always goes through.
if (evasive && event.detail !== 0) {
event.preventDefault();
return;
}
onClick?.(event);
if (state === "tired" && resetAfter > 0) {
window.clearTimeout(resetTimerRef.current);
resetTimerRef.current = window.setTimeout(() => {
staminaRef.current = null;
stamina.set(1);
setState("fleeing");
}, resetAfter);
}
};
const idleLabel = children ?? label;
const currentLabel = state === "tired" ? surrenderLabel : idleLabel;
return (
<span ref={wrapperRef} className="relative inline-flex">
<motion.button
ref={ref}
type={type}
disabled={disabled}
style={{ x, y, rotate }}
onPointerDown={handlePointerDown}
onClick={handleClick}
animate={
state === "tired" && !reduceMotion
? { scaleY: [1, 0.92, 1.03, 1], scaleX: [1, 1.05, 0.98, 1] }
: undefined
}
transition={{ duration: 0.45, ease: "easeOut" }}
className={cn(
"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
// Motion owns `transform` for the dodge, lean, and squash, so the press animates the separate `scale` property.
"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
// Press eases in fast and settles the shadow; release springs back on the slower base curve.
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<span className="relative inline-grid">
{/* Invisible copies reserve the wider label so the button never resizes. */}
<span
aria-hidden
className="invisible col-start-1 row-start-1 whitespace-nowrap"
>
{idleLabel}
</span>
<span
aria-hidden
className="invisible col-start-1 row-start-1 whitespace-nowrap"
>
{surrenderLabel}
</span>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
key={state}
variants={labelVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ type: "spring", stiffness: 420, damping: 30 }}
className="col-start-1 row-start-1 whitespace-nowrap text-center"
>
{currentLabel}
</motion.span>
</AnimatePresence>
</span>
{/* Stamina drains while the button is being chased. */}
<motion.span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-left bg-neutral-50/35"
style={{
scaleX: stamina,
opacity: state === "fleeing" ? staminaOpacity : 0,
}}
/>
</motion.button>
</span>
);
},
);
TrollButton.displayName = "TrollButton";
export { TrollButton };
export default TrollButton;
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
demo.tsx
export { ButtonDemo as default } from "@/import-examples/troll-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-neutral-50: oklch(98.5% 0 none);
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--font-weight-medium: 500;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--default-mono-font-family: var(--font-geist-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 {
.pointer-events-none {
pointer-events: none;
}
.invisible {
visibility: hidden;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-x-0 {
inset-inline: 0px;
}
.bottom-0 {
bottom: 0px;
}
.col-start-1 {
grid-column-start: 1;
}
.row-start-1 {
grid-row-start: 1;
}
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.h-0\.5 {
height: calc(var(--spacing) * 0.5);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.origin-left {
transform-origin: 0;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.cursor-pointer {
cursor: pointer;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.overflow-hidden {
overflow: hidden;
}
.rounded-\[10px\] {
border-radius: 10px;
}
.bg-neutral-50\/35 {
background-color: color-mix(in srgb, oklch(98.5% 0 none) 35%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-neutral-50) 35%, transparent);
}
}
.bg-linear-to-b {
--tw-gradient-position: to bottom;
@supports (background-image: linear-gradient(in lab, red, red)) {
--tw-gradient-position: to bottom in oklab;
}
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-\[\#353535\] {
--tw-gradient-from: #353535;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-\[\#272727\] {
--tw-gradient-to: #272727;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.text-center {
text-align: center;
}
.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);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-neutral-50 {
color: var(--color-neutral-50);
}
.shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.06\)\,inset_0_1px_0_rgb\(255_255_255\/0\.14\)\,0_1px_2px_rgb\(0_0_0\/0\.25\)\,0_4px_12px_-4px_rgb\(0_0_0\/0\.4\)\] {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.06)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.14)), 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.25)), 0 4px 12px -4px var(--tw-shadow-color, rgb(0 0 0/0.4));
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-\[scale\,filter\,box-shadow\] {
transition-property: scale,filter,box-shadow;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-300 {
--tw-duration: 300ms;
transition-duration: 300ms;
}
.ease-\[cubic-bezier\(0\.34\,1\.35\,0\.64\,1\)\] {
--tw-ease: cubic-bezier(0.34,1.35,0.64,1);
transition-timing-function: cubic-bezier(0.34,1.35,0.64,1);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
@media (hover: hover) {
.hover\:brightness-110:hover {
--tw-brightness: brightness(110%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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\:ring-offset-background:focus-visible {
--tw-ring-offset-color: var(--background);
}
.active\:scale-\[0\.97\]:active {
scale: 0.97;
}
.active\:shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.05\)\,inset_0_1px_0_rgb\(255_255_255\/0\.08\)\,0_0_1px_rgb\(0_0_0\/0\.2\)\,0_1px_3px_-2px_rgb\(0_0_0\/0\.3\)\]:active {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.05)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.08)), 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.2)), 0 1px 3px -2px var(--tw-shadow-color, rgb(0 0 0/0.3));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.active\:brightness-95:active {
--tw-brightness: brightness(95%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.active\:duration-100:active {
--tw-duration: 100ms;
transition-duration: 100ms;
}
.active\:ease-out:active {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:active\:scale-100:active {
--tw-scale-x: 100%;
--tw-scale-y: 100%;
--tw-scale-z: 100%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
}
: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;
}
:root {
--card: oklch(1 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--primary: oklch(0.205 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--chrome: oklch(0.967 0 0);
--brand: oklch(0.6 0.23 29);
--chart-background: oklch(1 0 0);
--chart-foreground: oklch(0.145 0.004 285);
--chart-foreground-muted: oklch(0.55 0.014 260);
--chart-line-primary: var(--chart-1);
--chart-line-secondary: var(--chart-2);
--chart-crosshair: oklch(0.4 0.1828 274.34);
--chart-grid: oklch(0.9 0 0);
--chart-brush-border: var(--chart-grid);
--chart-tooltip-background: oklch(0.21 0.006 285 / 0.8);
--chart-tooltip-foreground: oklch(0.985 0 0);
--chart-tooltip-muted: oklch(0.65 0.01 260);
--chart-marker-background: oklch(0.97 0.005 260);
--chart-marker-border: oklch(0.85 0.01 260);
--chart-marker-foreground: oklch(0.3 0.01 260);
--chart-label: oklch(0.45 0.01 260);
--chart-scale-01: oklch(0.98 0.003 106);
--chart-scale-02: oklch(0.92 0.008 106);
--chart-scale-03: oklch(0.82 0.015 106);
--chart-scale-04: oklch(0.68 0.02 106);
--chart-scale-05: oklch(0.55 0.025 106);
--chart-scale-pattern-color: oklch(0.96 0.005 106);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--chrome: oklch(0.18 0 0);
--card: oklch(0.205 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--primary: oklch(0.922 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.922 0 0);
--sidebar-primary-foreground: oklch(0.205 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--brand: oklch(0.66 0.23 32);
--chart-background: oklch(0.145 0 0);
--chart-foreground: oklch(0.45 0 0);
--chart-foreground-muted: oklch(0.65 0.01 260);
--chart-crosshair: oklch(0.45 0 0);
--chart-grid: oklch(0.25 0 0);
--chart-brush-border: var(--chart-grid);
--chart-marker-background: oklch(0.25 0.01 260);
--chart-marker-border: oklch(0.4 0.01 260);
--chart-marker-foreground: oklch(0.9 0 0);
--chart-label: oklch(0.75 0.01 260);
--chart-scale-01: oklch(0.28 0.03 270);
--chart-scale-02: oklch(0.38 0.04 270);
--chart-scale-03: oklch(0.48 0.05 270);
--chart-scale-04: oklch(0.58 0.06 270);
--chart-scale-05: oklch(0.68 0.07 270);
--chart-scale-pattern-color: oklch(0.145 0 0);
}
@keyframes docs-dot-swing {
0%, 100% {
transform: translateX(0) scale(1);
}
45% {
transform: translateX(-6px) scale(0.7);
}
}
@keyframes hell-flicker {
0%, 100% {
opacity: 1;
}
41% {
opacity: 0.7;
}
43% {
opacity: 1;
}
77% {
opacity: 0.85;
}
78% {
opacity: 1;
}
}
@keyframes lights-off {
0% {
opacity: 0;
}
15% {
opacity: 1;
}
30% {
opacity: 0.2;
}
45% {
opacity: 1;
}
60% {
opacity: 0.5;
}
70%, 100% {
opacity: 1;
}
}
@keyframes lights-on {
0% {
opacity: 1;
}
20% {
opacity: 0;
}
32% {
opacity: 1;
}
50% {
opacity: 0.1;
}
60% {
opacity: 0.9;
}
75%, 100% {
opacity: 0;
}
}
@keyframes lights-fade {
to {
opacity: 0;
}
}
@keyframes eyes-dim {
from {
opacity: 0;
}
}
@keyframes eyes-open {
from {
opacity: 0;
transform: scaleY(0.05);
}
}
@keyframes eyes-blink {
0%, 92%, 100% {
transform: none;
}
95% {
transform: scaleY(0.08);
}
}
@keyframes crack-life {
0% {
opacity: 0;
transform: scale(0.92);
}
4% {
opacity: 1;
transform: none;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes egg-wobble {
0%, 84%, 100% {
transform: none;
}
87% {
transform: rotate(-14deg);
}
90% {
transform: rotate(10deg);
}
93% {
transform: rotate(-6deg);
}
96% {
transform: rotate(3deg);
}
}
@keyframes page-glitch {
0% {
transform: none;
filter: none;
clip-path: none;
}
12% {
transform: translate(-6px, 2px) skewX(-2deg);
filter: hue-rotate(90deg) saturate(3);
}
24% {
transform: translate(5px, -1px);
filter: invert(1) hue-rotate(180deg);
clip-path: inset(18% 0 52% 0);
}
36% {
transform: none;
filter: contrast(2.2) saturate(0);
clip-path: none;
}
48% {
transform: translate(9px, 0) skewX(4deg);
filter: hue-rotate(-60deg);
clip-path: inset(0 0 70% 0);
}
60% {
transform: translate(-3px, 3px);
filter: none;
clip-path: inset(40% 0 20% 0);
}
72% {
transform: translate(2px, -4px) scaleY(1.02);
filter: saturate(4) contrast(1.4);
clip-path: none;
}
86% {
transform: translate(-8px, 0);
filter: invert(1);
clip-path: inset(62% 0 8% 0);
}
}
@keyframes glitch-label {
0% {
transform: translate(0, 0);
opacity: 1;
}
33% {
transform: translate(-4px, 2px);
opacity: 0.6;
}
66% {
transform: translate(5px, -2px);
opacity: 1;
}
}
@keyframes deleted-progress {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes deleted-restore {
0%, 70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes creeper-swell {
0% {
transform: scale(0.3) translateY(40px);
opacity: 0;
filter: none;
}
20% {
transform: scale(1);
opacity: 1;
}
45%, 65%, 85% {
filter: brightness(3) saturate(0);
}
55%, 75% {
filter: none;
}
100% {
transform: scale(1.35);
filter: brightness(4) saturate(0);
}
}
@keyframes creeper-flash {
from {
opacity: 0.85;
}
to {
opacity: 0;
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-gradient-position {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@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;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@property --tw-scale-x {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-y {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-z {
syntax: "*";
inherits: false;
initial-value: 1;
}
@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-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--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;
--tw-duration: initial;
--tw-ease: initial;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
}
}
}