{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drip-badge",
  "type": "registry:component",
  "title": "Drip Badge",
  "description": "A notification count made of liquid: a new one makes a droplet drip off the bottom, reading one pinches a piece off the side, and the number rolls either way.",
  "categories": [
    "component",
    "Liquid"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/drip-badge.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TONES = {\n  destructive: \"[--drip:var(--color-destructive)] text-white\",\n  primary: \"[--drip:var(--color-primary)] text-primary-foreground\",\n  info: \"[--drip:var(--color-info)] text-white\",\n  success: \"[--drip:var(--color-success)] text-white\",\n} as const;\n\nconst H = 18;\nconst PAD = 14;\n\nconst roll = {\n  enter: (d: number) => ({ y: d * 9, opacity: 0 }),\n  center: { y: 0, opacity: 1 },\n  exit: (d: number) => ({ y: -d * 9, opacity: 0 }),\n};\n// Same keys as `roll` so the server and a reduced-motion client render identical styles.\nconst fade = { enter: { y: 0, opacity: 0 }, center: { y: 0, opacity: 1 }, exit: { y: 0, opacity: 0 } };\n\ntype Splash = { id: number; kind: \"drip\" | \"split\" };\n\nexport interface DripBadgeProps {\n  count: number;\n  max?: number;\n  tone?: keyof typeof TONES;\n  children: ReactNode;\n  showZero?: boolean;\n  className?: string;\n}\n\nexport function DripBadge({ count, max = 99, tone = \"destructive\", children, showZero = false, className }: DripBadgeProps) {\n  const reduce = useReducedMotion();\n  const filterId = `drip-badge-${useId().replace(/:/g, \"\")}`;\n  const previous = useRef(count);\n  const nextId = useRef(0);\n  const [splashes, setSplashes] = useState<Splash[]>([]);\n  const [shown, setShown] = useState(count);\n  const [direction, setDirection] = useState<1 | -1>(1);\n  if (count !== shown) {\n    setShown(count);\n    setDirection(count > shown ? 1 : -1);\n  }\n\n  useEffect(() => {\n    const before = previous.current;\n    previous.current = count;\n    if (count === before) return;\n    if (reduce || (count === 0 && !showZero) || (before === 0 && !showZero)) return;\n    const splash: Splash = { id: nextId.current++, kind: count > before ? \"drip\" : \"split\" };\n    setSplashes((s) => [...s.slice(-2), splash]);\n  }, [count, reduce, showZero]);\n\n  const visible = count > 0 || showZero;\n  const label = count > max ? `${max}+` : String(count);\n  const width = Math.max(H, Math.round(label.length * 6.6 + 10));\n\n  return (\n    <span className={cn(\"relative inline-flex\", className)}>\n      {children}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {count === 0 ? \"No unread notifications\" : `${count} unread`}\n      </span>\n      <span\n        aria-hidden\n        className={cn(\"pointer-events-none absolute\", TONES[tone])}\n        style={{ top: -6 - PAD, left: `calc(100% - ${12 + PAD}px)`, width: 48 + PAD * 2, height: H + PAD * 2 + 22 }}\n      >\n        <svg className=\"absolute size-0\">\n          <filter colorInterpolationFilters=\"sRGB\" height=\"100%\" id={filterId} width=\"100%\" x=\"0\" y=\"0\">\n            <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation=\"2.6\" />\n            <feColorMatrix in=\"blur\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 24 -10\" />\n          </filter>\n        </svg>\n        <span className=\"absolute inset-0\" style={{ filter: `url(#${filterId})` }}>\n          <AnimatePresence initial={false}>\n            {visible && (\n              <motion.span\n                animate={{ scale: 1, opacity: 1, width }}\n                className=\"absolute rounded-full bg-(--drip)\"\n                exit={{ scale: 0.5, opacity: 0 }}\n                initial={{ scale: 0.5, opacity: 0, width }}\n                key=\"pill\"\n                style={{ left: PAD, top: PAD, height: H, originX: 0.5 }}\n                transition={{ type: \"spring\", stiffness: 520, damping: 30 }}\n              />\n            )}\n          </AnimatePresence>\n          {splashes.map((splash) =>\n            splash.kind === \"drip\" ? (\n              <motion.span\n                animate={{ y: [0, 6, 15, 32], scale: [0.6, 1, 0.92, 0.7], opacity: [1, 1, 1, 0] }}\n                className=\"absolute rounded-full bg-(--drip)\"\n                key={splash.id}\n                onAnimationComplete={() => setSplashes((s) => s.filter((x) => x.id !== splash.id))}\n                style={{ left: PAD + width / 2 - 4, top: PAD + H - 9, width: 8, height: 8 }}\n                transition={{ duration: 0.7, times: [0, 0.3, 0.55, 1], ease: [\"easeOut\", \"easeIn\", \"easeIn\"] }}\n              />\n            ) : (\n              <motion.span\n                animate={{ x: [0, 9, 20], scale: [1, 0.85, 0.2], opacity: [1, 1, 0] }}\n                className=\"absolute rounded-full bg-(--drip)\"\n                key={splash.id}\n                onAnimationComplete={() => setSplashes((s) => s.filter((x) => x.id !== splash.id))}\n                style={{ left: PAD + width - 12, top: PAD + (H - 12) / 2, width: 12, height: 12 }}\n                transition={{ duration: 0.5, times: [0, 0.4, 1], ease: [\"easeOut\", \"easeIn\"] }}\n              />\n            ),\n          )}\n        </span>\n        <AnimatePresence initial={false}>\n          {visible && (\n            <motion.span\n              animate={{ opacity: 1, scale: 1, width }}\n              className=\"absolute flex items-center justify-center overflow-hidden font-semibold text-[11px] tabular-nums leading-none\"\n              exit={{ opacity: 0, scale: 0.5 }}\n              initial={{ opacity: 0, scale: 0.5, width }}\n              key=\"label\"\n              style={{ left: PAD, top: PAD, height: H }}\n              transition={{ type: \"spring\", stiffness: 520, damping: 30 }}\n            >\n              <AnimatePresence custom={direction} initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate=\"center\"\n                  custom={direction}\n                  exit=\"exit\"\n                  initial=\"enter\"\n                  key={label}\n                  variants={reduce ? fade : roll}\n                  transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n                >\n                  {label}\n                </motion.span>\n              </AnimatePresence>\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n}\n"
    }
  ]
}