Back to buttons
"use client"

import { CSSProperties, useEffect, useRef, useState } from "react"
import {Check,ChevronRight} from "lucide-react";

import { cn } from "@/lib/utils"

export interface SwipeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
  onSwipeComplete?: () => void
  text?: string
  className?: string
  gap?: number
  validationDuration?: number
}

export function SwipeButton({
  onSwipeComplete,
  text = "Swipe to validate",
  className,
  gap = 3,
  validationDuration = 2000,
  ...props
}: SwipeButtonProps) {
  const [isSwiped, setIsSwiped] = useState(false)
  const [isValidated, setIsValidated] = useState(false)
  const [startX, setStartX] = useState(0)
  const [currentX, setCurrentX] = useState(0)
  const [isDragging, setIsDragging] = useState(false)
  const containerRef = useRef<HTMLDivElement>(null)
  const buttonRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (isValidated) {
      const timer = setTimeout(() => {
        setIsValidated(false)
        setIsSwiped(false)
        setCurrentX(0)
        setIsDragging(false)
      }, validationDuration)
      return () => clearTimeout(timer)
    }
  }, [isValidated, validationDuration])

  const handleStart = (clientX: number) => {
    if (isValidated) return
    setStartX(clientX)
    setIsDragging(true)
  }

  const handleMove = (clientX: number) => {
    if (!buttonRef.current || !isDragging || isValidated) return

    const containerWidth = containerRef.current?.offsetWidth || 0
    const buttonWidth = buttonRef.current.offsetWidth
    const maxSwipe = containerWidth - buttonWidth - gap * 2

    let newX = clientX - startX
    newX = Math.max(0, Math.min(newX, maxSwipe))

    setCurrentX(newX)
    setIsSwiped(newX >= maxSwipe - 10)
  }

  const handleEnd = () => {
    if (isValidated) return

    if (isSwiped) {
      setIsValidated(true)
      setCurrentX(0)
      onSwipeComplete?.()
    } else {
      setCurrentX(0)
      setIsSwiped(false)
    }
    setIsDragging(false)
  }

  return (
    <div
      ref={containerRef}
      className={cn(
        "relative h-10 w-[250px] overflow-hidden rounded-lg",
        "border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900",
        "transition-colors duration-200",
        className
      )}
      onTouchStart={(e) => handleStart(e.touches[0].clientX)}
      onTouchMove={(e) => handleMove(e.touches[0].clientX)}
      onTouchEnd={handleEnd}
      onMouseDown={(e) => handleStart(e.clientX)}
      onMouseMove={(e) => handleMove(e.clientX)}
      onMouseUp={handleEnd}
      onMouseLeave={handleEnd}
      role="button"
      aria-label="Swipe to validate"
      {...props}
    >
      <button
        ref={buttonRef}
        className={cn(
          "absolute rounded-md",
          "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900",
          "flex items-center justify-center",
          "cursor-grab active:cursor-grabbing",
          "shadow-sm transition-all duration-300",
          "hover:bg-neutral-800 dark:hover:bg-neutral-100",
          "focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:outline-none dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900",
          "disabled:pointer-events-none",
          isValidated &&
            "w-[calc(100%-6px)] cursor-default bg-emerald-500 opacity-100 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-500"
        )}
        style={{
          width: isValidated ? `calc(100% - ${gap * 2}px)` : "36px",
          height: `calc(100% - ${gap * 2}px)`,
          left: isValidated ? `${gap}px` : `${gap}px`,
          top: `${gap}px`,
          transform: isValidated ? "none" : `translateX(${currentX}px)`,
          transition: isDragging ? "none" : "all 0.3s ease",
        }}
        aria-label={isValidated ? "Validated" : "Swipe to validate"}
        disabled={isValidated}
      >
        {isValidated ? (
          <Check className="h-4 w-4" aria-hidden="true" />
        ) : (
          <ChevronRight className="h-4 w-4" aria-hidden="true" />
        )}
      </button>
      <div className="flex h-full w-full items-center justify-center">
        <span
          style={{ "--swipe-button-text-width": "130px" } as CSSProperties}
          className={cn(
            "pointer-events-none mx-auto max-w-md text-sm text-neutral-600/70 dark:text-neutral-400/70",
            "animate-swipe-button-text [background-size:var(--swipe-button-text-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat select-none [transition:background-position_1s_cubic-bezier(.4,0,.2,1)_infinite]",
            "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80"
          )}
        >
          {text}
        </span>
      </div>
    </div>
  )
}

Swipe Button

By badtz

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

About Swipe Button

Swipe Button is an animated React and Tailwind CSS button by badtz. 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

registry/new-york-v4/examples/swipe-button-demo.tsx

"use client"

import confetti from "canvas-confetti"

import { SwipeButton } from "@/registry/new-york-v4/ui/swipe-button"

export default function SwipeButtonDemo() {
  const triggerConfetti = () => {
    confetti({
      particleCount: 100,
      spread: 70,
      origin: { y: 0.6 },
    })
  }

  return (
    <div>
      <SwipeButton onSwipeComplete={triggerConfetti} />
    </div>
  )
}

registry/new-york-v4/ui/swipe-button.tsx

"use client"

import { CSSProperties, useEffect, useRef, useState } from "react"
import {Check,ChevronRight} from "lucide-react";

import { cn } from "@/lib/utils"

export interface SwipeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
  onSwipeComplete?: () => void
  text?: string
  className?: string
  gap?: number
  validationDuration?: number
}

export function SwipeButton({
  onSwipeComplete,
  text = "Swipe to validate",
  className,
  gap = 3,
  validationDuration = 2000,
  ...props
}: SwipeButtonProps) {
  const [isSwiped, setIsSwiped] = useState(false)
  const [isValidated, setIsValidated] = useState(false)
  const [startX, setStartX] = useState(0)
  const [currentX, setCurrentX] = useState(0)
  const [isDragging, setIsDragging] = useState(false)
  const containerRef = useRef<HTMLDivElement>(null)
  const buttonRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (isValidated) {
      const timer = setTimeout(() => {
        setIsValidated(false)
        setIsSwiped(false)
        setCurrentX(0)
        setIsDragging(false)
      }, validationDuration)
      return () => clearTimeout(timer)
    }
  }, [isValidated, validationDuration])

  const handleStart = (clientX: number) => {
    if (isValidated) return
    setStartX(clientX)
    setIsDragging(true)
  }

  const handleMove = (clientX: number) => {
    if (!buttonRef.current || !isDragging || isValidated) return

    const containerWidth = containerRef.current?.offsetWidth || 0
    const buttonWidth = buttonRef.current.offsetWidth
    const maxSwipe = containerWidth - buttonWidth - gap * 2

    let newX = clientX - startX
    newX = Math.max(0, Math.min(newX, maxSwipe))

    setCurrentX(newX)
    setIsSwiped(newX >= maxSwipe - 10)
  }

  const handleEnd = () => {
    if (isValidated) return

    if (isSwiped) {
      setIsValidated(true)
      setCurrentX(0)
      onSwipeComplete?.()
    } else {
      setCurrentX(0)
      setIsSwiped(false)
    }
    setIsDragging(false)
  }

  return (
    <div
      ref={containerRef}
      className={cn(
        "relative h-10 w-[250px] overflow-hidden rounded-lg",
        "border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900",
        "transition-colors duration-200",
        className
      )}
      onTouchStart={(e) => handleStart(e.touches[0].clientX)}
      onTouchMove={(e) => handleMove(e.touches[0].clientX)}
      onTouchEnd={handleEnd}
      onMouseDown={(e) => handleStart(e.clientX)}
      onMouseMove={(e) => handleMove(e.clientX)}
      onMouseUp={handleEnd}
      onMouseLeave={handleEnd}
      role="button"
      aria-label="Swipe to validate"
      {...props}
    >
      <button
        ref={buttonRef}
        className={cn(
          "absolute rounded-md",
          "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900",
          "flex items-center justify-center",
          "cursor-grab active:cursor-grabbing",
          "shadow-sm transition-all duration-300",
          "hover:bg-neutral-800 dark:hover:bg-neutral-100",
          "focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:outline-none dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900",
          "disabled:pointer-events-none",
          isValidated &&
            "w-[calc(100%-6px)] cursor-default bg-emerald-500 opacity-100 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-500"
        )}
        style={{
          width: isValidated ? `calc(100% - ${gap * 2}px)` : "36px",
          height: `calc(100% - ${gap * 2}px)`,
          left: isValidated ? `${gap}px` : `${gap}px`,
          top: `${gap}px`,
          transform: isValidated ? "none" : `translateX(${currentX}px)`,
          transition: isDragging ? "none" : "all 0.3s ease",
        }}
        aria-label={isValidated ? "Validated" : "Swipe to validate"}
        disabled={isValidated}
      >
        {isValidated ? (
          <Check className="h-4 w-4" aria-hidden="true" />
        ) : (
          <ChevronRight className="h-4 w-4" aria-hidden="true" />
        )}
      </button>
      <div className="flex h-full w-full items-center justify-center">
        <span
          style={{ "--swipe-button-text-width": "130px" } as CSSProperties}
          className={cn(
            "pointer-events-none mx-auto max-w-md text-sm text-neutral-600/70 dark:text-neutral-400/70",
            "animate-swipe-button-text [background-size:var(--swipe-button-text-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat select-none [transition:background-position_1s_cubic-bezier(.4,0,.2,1)_infinite]",
            "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80"
          )}
        >
          {text}
        </span>
      </div>
    </div>
  )
}

lib/utils.ts

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

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

export function absoluteUrl(path: string) {
  return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}

demo.tsx

export { default as default } from "@/registry/new-york-v4/examples/swipe-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: var(--font-sans);
    --font-mono: var(--font-mono);
    --color-emerald-500: oklch(69.6% 0.17 162.48);
    --color-blue-300: oklch(80.9% 0.105 251.813);
    --color-blue-500: oklch(62.3% 0.214 259.815);
    --color-blue-600: oklch(54.6% 0.245 262.881);
    --color-blue-700: oklch(48.8% 0.243 264.376);
    --color-blue-800: oklch(42.4% 0.199 265.638);
    --color-neutral-100: oklch(97% 0 none);
    --color-neutral-200: oklch(92.2% 0 none);
    --color-neutral-400: oklch(70.8% 0 none);
    --color-neutral-600: oklch(43.9% 0 none);
    --color-neutral-800: oklch(26.9% 0 none);
    --color-neutral-900: oklch(20.5% 0 none);
    --color-black: #000;
    --color-white: #fff;
    --spacing: 0.25rem;
    --container-md: 28rem;
    --text-sm: 0.875rem;
    --text-sm--line-height: calc(1.25 / 0.875);
    --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);
    --font-instrument-serif: var(--font-instrument-serif);
  }
}
@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;
  }
  .absolute {
    position: absolute;
  }
  .relative {
    position: relative;
  }
  .mx-auto {
    margin-inline: auto;
  }
  .flex {
    display: flex;
  }
  .h-4 {
    height: calc(var(--spacing) * 4);
  }
  .h-10 {
    height: calc(var(--spacing) * 10);
  }
  .h-full {
    height: 100%;
  }
  .w-4 {
    width: calc(var(--spacing) * 4);
  }
  .w-\[250px\] {
    width: 250px;
  }
  .w-\[calc\(100\%-6px\)\] {
    width: calc(100% - 6px);
  }
  .w-full {
    width: 100%;
  }
  .max-w-md {
    max-width: var(--container-md);
  }
  .transform {
    transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
  }
  .animate-swipe-button-text {
    animation: swipe-button-text 1.5s
    cubic-bezier(0.4, 0, 0.2, 1) infinite;
  }
  .cursor-default {
    cursor: default;
  }
  .cursor-grab {
    cursor: grab;
  }
  .items-center {
    align-items: center;
  }
  .justify-center {
    justify-content: center;
  }
  .overflow-hidden {
    overflow: hidden;
  }
  .rounded-lg {
    border-radius: var(--radius);
  }
  .rounded-md {
    border-radius: calc(var(--radius) - 2px);
  }
  .border {
    border-style: var(--tw-border-style);
    border-width: 1px;
  }
  .border-neutral-200 {
    border-color: var(--color-neutral-200);
  }
  .bg-emerald-500 {
    background-color: var(--color-emerald-500);
  }
  .bg-neutral-900 {
    background-color: var(--color-neutral-900);
  }
  .bg-white {
    background-color: var(--color-white);
  }
  .bg-gradient-to-r {
    --tw-gradient-position: to right in oklab;
    background-image: linear-gradient(var(--tw-gradient-stops));
  }
  .from-transparent {
    --tw-gradient-from: transparent;
    --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));
  }
  .via-black\/80 {
    --tw-gradient-via: color-mix(in srgb, #000 80%, transparent);
    @supports (color: color-mix(in lab, red, red)) {
      --tw-gradient-via: color-mix(in oklab, var(--color-black) 80%, transparent);
    }
    --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
    --tw-gradient-stops: var(--tw-gradient-via-stops);
  }
  .via-50\% {
    --tw-gradient-via-position: 50%;
  }
  .to-transparent {
    --tw-gradient-to: transparent;
    --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));
  }
  .\[background-size\:var\(--swipe-button-text-width\)_100\%\] {
    background-size: var(--swipe-button-text-width) 100%;
  }
  .bg-clip-text {
    background-clip: text;
  }
  .\[background-position\:0_0\] {
    background-position: 0 0;
  }
  .bg-no-repeat {
    background-repeat: no-repeat;
  }
  .text-sm {
    font-size: var(--text-sm);
    line-height: var(--tw-leading, var(--text-sm--line-height));
  }
  .text-neutral-600\/70 {
    color: color-mix(in srgb, oklch(43.9% 0 none) 70%, transparent);
    @supports (color: color-mix(in lab, red, red)) {
      color: color-mix(in oklab, var(--color-neutral-600) 70%, transparent);
    }
  }
  .text-white {
    color: var(--color-white);
  }
  .opacity-100 {
    opacity: 100%;
  }
  .shadow-sm {
    --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px 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-all {
    transition-property: all;
    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));
  }
  .duration-200 {
    --tw-duration: 200ms;
    transition-duration: 200ms;
  }
  .duration-300 {
    --tw-duration: 300ms;
    transition-duration: 300ms;
  }
  .select-none {
    -webkit-user-select: none;
    user-select: none;
  }
  .\[transition\:background-position_1s_cubic-bezier\(\.4\,0\,\.2\,1\)_infinite\] {
    transition: background-position 1s cubic-bezier(.4,0,.2,1) infinite;
  }
  @media (hover: hover) {
    .hover\:bg-emerald-500:hover {
      background-color: var(--color-emerald-500);
    }
    .hover\:bg-neutral-800:hover {
      background-color: var(--color-neutral-800);
    }
  }
  .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-neutral-400:focus-visible {
    --tw-ring-color: var(--color-neutral-400);
  }
  .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;
  }
  .active\:cursor-grabbing:active {
    cursor: grabbing;
  }
  .disabled\:pointer-events-none:disabled {
    pointer-events: none;
  }
  .dark\:border-neutral-800:where(.dark, .dark *) {
    border-color: var(--color-neutral-800);
  }
  .dark\:bg-emerald-500:where(.dark, .dark *) {
    background-color: var(--color-emerald-500);
  }
  .dark\:bg-neutral-900:where(.dark, .dark *) {
    background-color: var(--color-neutral-900);
  }
  .dark\:bg-white:where(.dark, .dark *) {
    background-color: var(--color-white);
  }
  .dark\:via-white\/80:where(.dark, .dark *) {
    --tw-gradient-via: color-mix(in srgb, #fff 80%, transparent);
    @supports (color: color-mix(in lab, red, red)) {
      --tw-gradient-via: color-mix(in oklab, var(--color-white) 80%, transparent);
    }
    --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
    --tw-gradient-stops: var(--tw-gradient-via-stops);
  }
  .dark\:text-neutral-400\/70:where(.dark, .dark *) {
    color: color-mix(in srgb, oklch(70.8% 0 none) 70%, transparent);
    @supports (color: color-mix(in lab, red, red)) {
      color: color-mix(in oklab, var(--color-neutral-400) 70%, transparent);
    }
  }
  .dark\:text-neutral-900:where(.dark, .dark *) {
    color: var(--color-neutral-900);
  }
  @media (hover: hover) {
    .dark\:hover\:bg-emerald-500:where(.dark, .dark *):hover {
      background-color: var(--color-emerald-500);
    }
    .dark\:hover\:bg-neutral-100:where(.dark, .dark *):hover {
      background-color: var(--color-neutral-100);
    }
  }
  .dark\:focus-visible\:ring-neutral-600:where(.dark, .dark *):focus-visible {
    --tw-ring-color: var(--color-neutral-600);
  }
  .dark\:focus-visible\:ring-offset-neutral-900:where(.dark, .dark *):focus-visible {
    --tw-ring-offset-color: var(--color-neutral-900);
  }
}
: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 {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.6049 0.1419 276.7);
  --primary-foreground: oklch(1 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.832 0.1638 83.82);
  --accent-foreground: oklch(0.205 0 0);
  --accent-border: oklch(0.602 0.1186 283.39);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: var(--color-blue-300);
  --chart-2: var(--color-blue-500);
  --chart-3: var(--color-blue-600);
  --chart-4: var(--color-blue-700);
  --chart-5: var(--color-blue-800);
  --sidebar: oklch(0.99 0 0);
  --sidebar-foreground: oklch(1 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);
  --surface: oklch(0.99 0 0);
  --surface-foreground: var(--foreground);
  --code: var(--surface);
  --code-foreground: var(--surface-foreground);
  --code-highlight: oklch(0.96 0 0);
  --code-number: oklch(0.56 0 0);
  --selection: oklch(0.832 0.1638 83.82);
  --selection-foreground: oklch(1 0 0);
}
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.269 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.6049 0.1419 276.7);
  --primary-foreground: oklch(1 0 0);
  --secondary: oklch(0.1826 0.002 286.2);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.5601 0.1884 258.36);
  --accent-foreground: oklch(0.985 0 0);
  --accent-border: oklch(0.602 0.1186 283.39);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: var(--color-blue-300);
  --chart-2: var(--color-blue-500);
  --chart-3: var(--color-blue-600);
  --chart-4: var(--color-blue-700);
  --chart-5: var(--color-blue-800);
  --sidebar: oklch(0.1826 0.002 286.2);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 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.439 0 0);
  --surface: oklch(0.1826 0.002 286.2);
  --surface-foreground: oklch(0.708 0 0);
  --code: var(--surface);
  --code-foreground: var(--surface-foreground);
  --code-highlight: oklch(0.27 0 0);
  --code-number: oklch(0.72 0 0);
  --selection: oklch(0.832 0.1638 83.82);
  --selection-foreground: oklch(1 0 0);
}
@keyframes infinite-ribbon {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100%);
  }
}
@keyframes infinite-ribbon-reverse {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(0);
  }
}
@keyframes marquee-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(calc(-100% - var(--gap)));
  }
}
@keyframes marquee-right {
  from {
    transform: translateX(calc(-100% - var(--gap)));
  }
  to {
    transform: translateX(0);
  }
}
@keyframes meteor {
  0% {
    transform: rotate(170deg) translateX(0);
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    transform: rotate(170deg) translateX(-500px);
    opacity: 0;
  }
}
@keyframes border-beam-flow {
  0% {
    offset-distance: 0%;
  }
  100% {
    offset-distance: 100%;
  }
}
@keyframes border-beam-rotate {
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
@keyframes svg-line-flow {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(33.33%);
  }
}
@keyframes flow-gradient {
  0% {
    background-position: -100% 0;
  }
  100% {
    background-position: 200% 0;
  }
}
@keyframes swipe-button-text {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: var(--swipe-button-text-width) 0;
  }
}
@keyframes star-btn {
  0% {
    offset-distance: 0%;
  }
  100% {
    offset-distance: 100%;
  }
}
@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-border-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}
@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-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;
}
@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-border-style: solid;
      --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-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;
    }
  }
}