{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flip-clock",
  "type": "registry:component",
  "title": "Flip Clock",
  "description": "A split-flap countdown where each changed digit's top flap falls away under gravity, darkening as it turns, and the new lower flap lands with one small rebound.",
  "categories": [
    "motion",
    "Text"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/flip-clock.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { type HTMLMotionProps, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst FALL = 0.24;\nconst LAND = 0.32;\n\n// Opaque on purpose: flaps stack, so an alpha token would show the layers through.\nconst tile =\n  \"bg-[color-mix(in_srgb,var(--color-foreground)_3%,var(--color-card))] dark:bg-[color-mix(in_srgb,var(--color-foreground)_7%,var(--color-card))]\";\n\ntype HalfProps = HTMLMotionProps<\"span\"> & { side: \"top\" | \"bottom\"; children: ReactNode };\n\nfunction Half({ side, children, className, ...props }: HalfProps) {\n  return (\n    <motion.span\n      className={cn(\n        \"absolute inset-x-0 h-1/2 overflow-hidden backface-hidden\",\n        tile,\n        side === \"top\" ? \"top-0 origin-bottom rounded-t-[inherit]\" : \"bottom-0 origin-top rounded-b-[inherit]\",\n        className,\n      )}\n      {...props}\n    >\n      <span className={cn(\"absolute inset-x-0 grid h-[200%] place-items-center leading-none\", side === \"top\" ? \"top-0\" : \"bottom-0\")}>\n        {children}\n      </span>\n    </motion.span>\n  );\n}\n\nexport interface FlipDigitProps {\n  value: string;\n  className?: string;\n}\n\nexport function FlipDigit({ value, className }: FlipDigitProps) {\n  const reduce = useReducedMotion();\n  const [shown, setShown] = useState({ current: value, previous: value, flip: 0 });\n\n  if (shown.current !== value) setShown((s) => ({ current: value, previous: s.current, flip: s.flip + 1 }));\n\n  const { current, previous, flip } = shown;\n  const flipping = !reduce && current !== previous;\n\n  return (\n    <span\n      className={cn(\n        \"relative inline-block h-[1.45em] w-[1.06em] rounded-[0.16em] font-mono font-medium tabular-nums shadow-xs/5 [perspective:4.5em]\",\n        className,\n      )}\n    >\n      <Half side=\"top\">{current}</Half>\n      <Half side=\"bottom\">{flipping ? previous : current}</Half>\n      {flipping && (\n        <>\n          <motion.span\n            animate={{ opacity: [0, 0.22] }}\n            className=\"pointer-events-none absolute inset-x-0 bottom-0 h-1/2 rounded-b-[inherit] bg-black\"\n            key={`shade-${flip}`}\n            transition={{ duration: FALL, ease: \"easeIn\" }}\n          />\n          <Half\n            animate={{ rotateX: -90 }}\n            initial={{ rotateX: 0 }}\n            key={`top-${flip}`}\n            side=\"top\"\n            transition={{ duration: FALL, ease: [0.55, 0, 1, 0.45] }}\n          >\n            {previous}\n            <motion.span\n              animate={{ opacity: 0.4 }}\n              className=\"absolute inset-0 bg-black\"\n              initial={{ opacity: 0 }}\n              transition={{ duration: FALL, ease: \"easeIn\" }}\n            />\n          </Half>\n          <Half\n            animate={{ rotateX: [90, 0, 6, 0] }}\n            initial={{ rotateX: 90 }}\n            key={`bottom-${flip}`}\n            onAnimationComplete={() => setShown((s) => (s.flip === flip ? { ...s, previous: s.current } : s))}\n            side=\"bottom\"\n            transition={{\n              delay: FALL,\n              duration: LAND,\n              times: [0, 0.55, 0.78, 1],\n              ease: [\n                [0.5, 0, 1, 0.6],\n                [0, 0, 0.4, 1],\n                [0.6, 0, 1, 1],\n              ],\n            }}\n          >\n            {current}\n            <motion.span\n              animate={{ opacity: 0 }}\n              className=\"absolute inset-0 bg-black\"\n              initial={{ opacity: 0.3 }}\n              transition={{ delay: FALL, duration: LAND * 0.55, ease: \"easeOut\" }}\n            />\n          </Half>\n        </>\n      )}\n      <span aria-hidden className=\"pointer-events-none absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-background/80\" />\n      <span aria-hidden className=\"pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-border ring-inset\" />\n    </span>\n  );\n}\n\nexport interface FlipClockUnit {\n  label: string;\n  value: number;\n  digits?: number;\n}\n\nexport interface FlipClockProps {\n  units: FlipClockUnit[];\n  label: string;\n  className?: string;\n}\n\nexport function FlipClock({ units, label, className }: FlipClockProps) {\n  return (\n    <div className={cn(\"flex items-start gap-[0.3em] text-[40px]\", className)} role=\"timer\">\n      <span className=\"sr-only\">{label}</span>\n      {units.map((u, i) => (\n        <div aria-hidden className=\"contents\" key={u.label}>\n          {i > 0 && (\n            <span className=\"flex h-[1.45em] flex-col justify-center gap-[0.22em] px-[0.04em]\">\n              <span className=\"size-[0.1em] rounded-full bg-muted-foreground/50\" />\n              <span className=\"size-[0.1em] rounded-full bg-muted-foreground/50\" />\n            </span>\n          )}\n          <div className=\"flex flex-col items-center gap-2\">\n            <div className=\"flex gap-[0.08em]\">\n              {String(u.value)\n                .padStart(u.digits ?? 2, \"0\")\n                .split(\"\")\n                .map((d, j) => (\n                  <FlipDigit key={j} value={d} />\n                ))}\n            </div>\n            <span className=\"text-muted-foreground text-xs\">{u.label}</span>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function useCountdown(target: number | null) {\n  const [now, setNow] = useState<number | null>(null);\n  useEffect(() => {\n    setNow(Date.now());\n    let id: ReturnType<typeof setTimeout>;\n    const tick = () => {\n      setNow(Date.now());\n      id = setTimeout(tick, 1000 - (Date.now() % 1000));\n    };\n    id = setTimeout(tick, 1000 - (Date.now() % 1000));\n    return () => clearTimeout(id);\n  }, []);\n  const left = target && now ? Math.max(0, Math.floor((target - now) / 1000)) : 0;\n  return {\n    ready: now !== null && target !== null,\n    days: Math.floor(left / 86400),\n    hours: Math.floor((left % 86400) / 3600),\n    minutes: Math.floor((left % 3600) / 60),\n    seconds: left % 60,\n  };\n}\n"
    }
  ]
}