{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flip-clock",
  "title": "Cyberpunk Button",
  "description": "Neat and clean functional clock ui with buttery smooth flip animations.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "packages/flip-clock/flip-clock.tsx",
      "content": "\"use client\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\nimport { cva, VariantProps } from \"class-variance-authority\";\r\nimport {\r\n  FC,\r\n  HTMLAttributes,\r\n  memo,\r\n  ReactNode,\r\n  useEffect,\r\n  useState,\r\n} from \"react\";\r\n\r\nconst flipUnitVariants = cva(\r\n  \"relative subpixel-antialiased perspective-[1000px] rounded-md overflow-hidden\",\r\n  {\r\n    variants: {\r\n      size: {\r\n        sm: \"w-10 min-w-10 h-14 text-3xl\", // Small (Compact UI)\r\n        md: \"w-14 min-w-14 h-20 text-5xl\", // Medium (Standard sidebar/header)\r\n        lg: \"w-17 min-w-17 h-24 text-6xl\", // Large (Focus/Hero)\r\n        xl: \"w-22 min-w-22 h-32 text-8xl\", // Extra Large (Dashboard/Landing)\r\n      },\r\n      variant: {\r\n        default: \"bg-primary text-primary-foreground\",\r\n        secondary: \"bg-secondary text-secondary-foreground\",\r\n        destructive: \"bg-destructive text-destructive-foreground\",\r\n        outline: \"border border-input bg-background text-foreground\",\r\n        muted: \"bg-muted text-muted-foreground\",\r\n      },\r\n    },\r\n    defaultVariants: {\r\n      size: \"md\",\r\n      variant: \"default\",\r\n    },\r\n  },\r\n);\r\n\r\ninterface FlipUnitProps\r\n  extends\r\n    HTMLAttributes<HTMLDivElement>,\r\n    VariantProps<typeof flipUnitVariants> {\r\n  digit: number | string;\r\n}\r\n\r\nconst commonCardStyle = cn(\r\n  \"absolute inset-x-0 overflow-hidden h-1/2 bg-inherit text-inherit\",\r\n);\r\n\r\nconst FlipUnit: FC<FlipUnitProps> = memo(function FlipUnit({\r\n  digit,\r\n  size,\r\n  variant,\r\n  className,\r\n}: FlipUnitProps) {\r\n  const [prevDigit, setPrevDigit] = useState(digit);\r\n  const [flipping, setFlipping] = useState(false);\r\n\r\n  useEffect(() => {\r\n    if (digit !== prevDigit) {\r\n      setFlipping(true);\r\n      // Wait for the full animation (0.3s top + 0.3s bottom) before resetting\r\n      const timer = setTimeout(() => {\r\n        setFlipping(false);\r\n        setPrevDigit(digit);\r\n      }, 550); // Slightly less than 600ms to ensure smoothness\r\n      return () => clearTimeout(timer);\r\n    }\r\n  }, [digit, prevDigit]);\r\n\r\n  return (\r\n    <div className={cn(flipUnitVariants({ size, variant }), className)}>\r\n      {/* 1. Background Top (The NEW digit waiting) */}\r\n      <div className={cn(commonCardStyle, \"rounded-t-lg top-0\")}>\r\n        <DigitSpan position=\"top\">{digit}</DigitSpan>\r\n      </div>\r\n\r\n      {/* 2. Background Bottom (The OLD digit staying) */}\r\n      <div className={cn(commonCardStyle, \"rounded-b-lg translate-y-full\")}>\r\n        <DigitSpan position=\"bottom\">{prevDigit}</DigitSpan>\r\n      </div>\r\n\r\n      {/* 3. Top Flap (The OLD digit falling down) */}\r\n      <div\r\n        className={cn(\r\n          commonCardStyle,\r\n          \"z-20 origin-bottom backface-hidden rounded-t-lg\",\r\n          flipping && \"animate-flip-top\",\r\n        )}\r\n      >\r\n        <DigitSpan position=\"top\">{prevDigit}</DigitSpan>\r\n      </div>\r\n\r\n      {/* 4. Bottom Flap (The NEW digit appearing) */}\r\n      <div\r\n        className={cn(\r\n          commonCardStyle,\r\n          \"z-10 origin-top backface-hidden rounded-b-lg translate-y-full\",\r\n          flipping && \"animate-flip-bottom\",\r\n        )}\r\n        style={{ transform: \"rotateX(90deg)\" }}\r\n      >\r\n        <DigitSpan position=\"bottom\">{digit}</DigitSpan>\r\n      </div>\r\n\r\n      {/* Center Divider Shadow */}\r\n      <div className=\"absolute top-1/2 left-0 w-full h-px -translate-y-1/2 bg-background/50 z-30\" />\r\n    </div>\r\n  );\r\n});\r\n\r\ninterface DigitSpanProps {\r\n  children: ReactNode;\r\n  position?: \"top\" | \"bottom\";\r\n}\r\n\r\nfunction DigitSpan({ children, position }: DigitSpanProps) {\r\n  return (\r\n    <span\r\n      className={cn(\r\n        \"absolute left-0 right-0 w-full flex items-center justify-center\",\r\n        // The span should be the full height of the PARENT FlipUnit (200% of the half-card)\r\n        \"h-[200%]\",\r\n      )}\r\n      style={{\r\n        // If it's the top half, align the full span to the top\r\n        // If it's the bottom half, shift the full span up so its bottom half shows\r\n        top: position === \"top\" ? \"0%\" : \"-100%\",\r\n      }}\r\n    >\r\n      {children}\r\n    </span>\r\n  );\r\n}\r\n\r\nconst flipClockVariants = cva(\r\n  \"relative flex justify-center items-center font-mono font-medium\",\r\n  {\r\n    variants: {\r\n      size: {\r\n        sm: \"text-3xl space-x-1\",\r\n        md: \"text-5xl space-x-2\",\r\n        lg: \"text-6xl space-x-2\",\r\n        xl: \"text-8xl space-x-3\",\r\n      },\r\n      variant: {\r\n        default: \"\",\r\n        secondary: \"\",\r\n        destructive: \"\",\r\n        outline: \"\",\r\n        muted: \"\",\r\n      },\r\n    },\r\n    defaultVariants: {\r\n      size: \"md\",\r\n      variant: \"default\",\r\n    },\r\n  },\r\n);\r\n\r\ninterface FlipClockProps\r\n  extends\r\n    VariantProps<typeof flipClockVariants>,\r\n    HTMLAttributes<HTMLDivElement> {\r\n  countdown?: boolean;\r\n  targetDate?: Date;\r\n  showDays?: \"auto\" | \"always\" | \"never\";\r\n}\r\n\r\ninterface TimeLeft {\r\n  days: number;\r\n  hours: number;\r\n  minutes: number;\r\n  seconds: number;\r\n}\r\n\r\ntype FlipClockSize = NonNullable<\r\n  VariantProps<typeof flipClockVariants>[\"size\"]\r\n>;\r\n\r\nconst heightMap: Record<FlipClockSize, string> = {\r\n  sm: \"text-4xl\",\r\n  md: \"text-5xl\",\r\n  lg: \"text-6xl\",\r\n  xl: \"text-8xl\",\r\n};\r\n\r\nfunction ClockSeparator({ size }: { size?: FlipClockSize }) {\r\n  return (\r\n    <span\r\n      className={cn(\r\n        \"text-center -translate-y-[8%]\",\r\n        size ? heightMap[size] : heightMap[\"md\"],\r\n      )}\r\n    >\r\n      :\r\n    </span>\r\n  );\r\n}\r\n\r\nconst FlipClock = ({\r\n  countdown = false,\r\n  targetDate,\r\n  size,\r\n  variant,\r\n  showDays = \"auto\",\r\n  className,\r\n  ...props\r\n}: FlipClockProps) => {\r\n  const [time, setTime] = useState<TimeLeft>(getTime(countdown, targetDate));\r\n\r\n  useEffect(() => {\r\n    // Run a faster heartbeat (250ms) to catch the second rollover immediately\r\n    const timer = setInterval(() => {\r\n      const nextTime = getTime(countdown, targetDate);\r\n\r\n      // Only update state if the seconds actually changed to prevent unnecessary re-renders\r\n      setTime((prev) => {\r\n        if (\r\n          prev.seconds === nextTime.seconds &&\r\n          prev.minutes === nextTime.minutes\r\n        ) {\r\n          return prev;\r\n        }\r\n        return nextTime;\r\n      });\r\n    }, 250); // 4fps check is plenty\r\n\r\n    return () => clearInterval(timer);\r\n  }, [countdown, targetDate]);\r\n\r\n  const daysStr = String(time.days).padStart(3, \"0\");\r\n  const hoursStr = String(time.hours).padStart(2, \"0\");\r\n  const minutesStr = String(time.minutes).padStart(2, \"0\");\r\n  const secondsStr = String(time.seconds).padStart(2, \"0\");\r\n\r\n  const shouldShowDays =\r\n    countdown &&\r\n    (showDays === \"always\" || (showDays === \"auto\" && time.days > 0));\r\n\r\n  return (\r\n    <div\r\n      className={cn(flipClockVariants({ size, variant }), className)}\r\n      aria-live=\"polite\"\r\n      {...props}\r\n    >\r\n      <span className=\"sr-only absolute\">\r\n        {`${time.hours}:${time.minutes}:${time.seconds}`}\r\n      </span>\r\n\r\n      {/* Days */}\r\n      {shouldShowDays && (\r\n        <>\r\n          {daysStr.split(\"\").map((digit, i) => (\r\n            <FlipUnit\r\n              key={`d-${i}`}\r\n              digit={digit}\r\n              size={size}\r\n              variant={variant}\r\n            />\r\n          ))}\r\n          <ClockSeparator size={size!} />\r\n        </>\r\n      )}\r\n\r\n      {/* Hours */}\r\n      {hoursStr.split(\"\").map((digit, index) => (\r\n        <FlipUnit\r\n          key={`hour-${index}`}\r\n          digit={digit}\r\n          size={size}\r\n          variant={variant}\r\n        />\r\n      ))}\r\n\r\n      <ClockSeparator size={size!} />\r\n\r\n      {/* Minutes */}\r\n      {minutesStr.split(\"\").map((digit, index) => (\r\n        <FlipUnit\r\n          key={`minute-${index}`}\r\n          digit={digit}\r\n          size={size}\r\n          variant={variant}\r\n        />\r\n      ))}\r\n\r\n      <ClockSeparator size={size!} />\r\n\r\n      {/* Seconds */}\r\n      {secondsStr.split(\"\").map((digit, index) => (\r\n        <FlipUnit\r\n          key={`second-${index}`}\r\n          digit={digit}\r\n          size={size}\r\n          variant={variant}\r\n        />\r\n      ))}\r\n\r\n      {/* Injected Keyframes (The Shadcn \"Cheat Code\") */}\r\n      <style jsx global>{`\r\n        /* Use the same duration for both to keep them in sync */\r\n        .animate-flip-top {\r\n          animation: flip-top-anim 0.6s ease-in forwards;\r\n        }\r\n        .animate-flip-bottom {\r\n          animation: flip-bottom-anim 0.6s ease-out forwards;\r\n        }\r\n\r\n        @keyframes flip-top-anim {\r\n          0% {\r\n            transform: rotateX(0deg);\r\n            z-index: 30;\r\n          }\r\n          50%,\r\n          100% {\r\n            transform: rotateX(-90deg);\r\n            z-index: 10;\r\n          }\r\n        }\r\n\r\n        @keyframes flip-bottom-anim {\r\n          0%,\r\n          50% {\r\n            transform: rotateX(90deg);\r\n            z-index: 10;\r\n          }\r\n          100% {\r\n            transform: rotateX(0deg);\r\n            z-index: 30;\r\n          }\r\n        }\r\n      `}</style>\r\n    </div>\r\n  );\r\n};\r\n\r\nfunction getTime(countdown: boolean, targetDate?: Date): TimeLeft {\r\n  const now = new Date();\r\n\r\n  // Real-time Clock Mode\r\n  if (!countdown) {\r\n    return {\r\n      days: 0,\r\n      hours: now.getHours(),\r\n      minutes: now.getMinutes(),\r\n      seconds: now.getSeconds(),\r\n    };\r\n  }\r\n\r\n  // Countdown Mode\r\n  if (!targetDate) return { days: 0, hours: 0, minutes: 0, seconds: 0 };\r\n  const diff = Math.max(0, targetDate.getTime() - now.getTime());\r\n\r\n  return {\r\n    days: Math.floor(diff / (1000 * 60 * 60 * 24)),\r\n    hours: Math.floor((diff / (1000 * 60 * 60)) % 24),\r\n    minutes: Math.floor((diff / (1000 * 60)) % 60),\r\n    seconds: Math.floor((diff / 1000) % 60),\r\n  };\r\n}\r\n\r\nexport default FlipClock;\r\n",
      "type": "registry:component",
      "target": "components/satori-ui/flip-clock.tsx"
    }
  ],
  "type": "registry:component"
}