{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-bell",
  "title": "Notification Bell",
  "description": "A bell icon button with a ring-shake swing, springy unread badge with rolling odometer count, and a pinging dot mode",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "app/registry/notification-bell/notification-bell.tsx",
      "content": "/**\n * Spectrum UI — NotificationBell\n *\n * A bell icon button micro-interaction. When the unread count increases the\n * bell swings from its hinge like a settling pendulum while the clapper\n * wiggles the opposite way in phase, and a rose badge springs in and rolls\n * its count like an odometer. A dot mode swaps the number for an indicator\n * that pings once per increase. Honors prefers-reduced-motion and announces\n * unread changes to screen readers.\n *\n * Dependencies: framer-motion, @/lib/utils\n *\n * @example\n * <NotificationBell count={3} onClick={() => openInbox()} />\n */\n\n\"use client\"\n\nimport React, { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface NotificationBellProps {\n  /** Number of unread notifications. Default 0 */\n  count?: number\n  /** Counts above this render as \"max+\". Default 99 */\n  max?: number\n  /** Show a small pinging dot instead of the numeric badge. Default false */\n  dot?: boolean\n  /** Play the ring swing once when the component mounts. Default false */\n  ringOnMount?: boolean\n  /** Click handler for the bell button */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>\n  /** Visual size of the button. Default \"md\" */\n  size?: \"sm\" | \"md\" | \"lg\"\n  className?: string\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Seconds a full ring swing takes to settle */\nconst SWING_DURATION = 0.9\n/** Bell rotation keyframes — amplitude decays like a released pendulum */\nconst BELL_SWING = [0, 15, -12, 8, -5, 3, -1.5, 0]\n/** Clapper counter-rotation; same keyframe count so it stays in phase */\nconst CLAPPER_SWING = [0, -17, 14, -10, 6, -3.5, 2, 0]\n/**\n * Keyframe times — a quick initial impulse, then near-constant half-periods\n * so the decay reads as physics rather than a linear ramp\n */\nconst SWING_TIMES = [0, 0.1, 0.26, 0.42, 0.58, 0.74, 0.88, 1]\n/** Ease per swing segment — slow at the extremes, fast through center */\nconst SWING_EASE = \"easeInOut\" as const\n\n/** Seconds the dot's ping halo takes to expand and fade */\nconst PING_DURATION = 0.9\n/** Badge entrance overshoots slightly — a positive \"you've got mail\" pop */\nconst BADGE_SPRING = { type: \"spring\", stiffness: 500, damping: 22 } as const\n/** Snappy micro spring — odometer roll and pressed feedback */\nconst COUNT_SPRING = { type: \"spring\", stiffness: 400, damping: 30 } as const\nconst TAP_SPRING = { type: \"spring\", stiffness: 500, damping: 30 } as const\n\nconst BELL_DOME_PATH = \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\"\nconst BELL_CLAPPER_PATH = \"M10.3 21a1.94 1.94 0 0 0 3.4 0\"\n\nconst SIZES = {\n  sm: { button: \"h-9 w-9\", icon: 16 },\n  md: { button: \"h-11 w-11\", icon: 20 },\n  lg: { button: \"h-[52px] w-[52px]\", icon: 24 },\n} as const\n\nconst countVariants = {\n  enter: (direction: number) => ({ y: direction * 10, opacity: 0 }),\n  center: { y: 0, opacity: 1 },\n  exit: (direction: number) => ({ y: direction * -10, opacity: 0 }),\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport function NotificationBell({\n  count = 0,\n  max = 99,\n  dot = false,\n  ringOnMount = false,\n  onClick,\n  size = \"md\",\n  className,\n}: NotificationBellProps) {\n  const shouldReduceMotion = useReducedMotion()\n  const prevCountRef = useRef(count)\n  // Monotonically increasing swing trigger — re-keying the bell restarts the\n  // swing cleanly on every increase instead of queueing animations\n  const [ringKey, setRingKey] = useState(() => (ringOnMount ? 1 : 0))\n\n  const { button: sizeClasses, icon } = SIZES[size]\n  const displayValue = count > max ? `${max}+` : String(count)\n  // While the count is rising, digits roll upward (and downward when falling)\n  const direction = count >= prevCountRef.current ? 1 : -1\n\n  useEffect(() => {\n    if (count > prevCountRef.current) setRingKey((key) => key + 1)\n    prevCountRef.current = count\n  }, [count])\n\n  const swinging = ringKey > 0 && !shouldReduceMotion\n\n  const swingTransition = swinging\n    ? { duration: SWING_DURATION, times: SWING_TIMES, ease: SWING_EASE }\n    : { duration: 0 }\n\n  const badgeTransition = shouldReduceMotion ? { duration: 0 } : BADGE_SPRING\n\n  return (\n    <motion.button\n      type=\"button\"\n      onClick={onClick}\n      aria-label={\n        count > 0 ? `Notifications, ${count} unread` : \"Notifications\"\n      }\n      whileTap={shouldReduceMotion ? undefined : { scale: 0.94 }}\n      transition={TAP_SPRING}\n      className={cn(\n        \"relative inline-flex touch-manipulation select-none items-center justify-center rounded-full border transition-colors\",\n        \"border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-100\",\n        \"dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800\",\n        \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-neutral-950 dark:focus-visible:ring-neutral-300\",\n        sizeClasses,\n        className,\n      )}\n    >\n      {/* Re-keying by ringKey replays the swing on every count increase; the\n          explicit rotate: 0 initial makes each remount start from rest */}\n      <motion.span\n        key={`bell-${ringKey}`}\n        className=\"inline-flex\"\n        style={{ transformOrigin: \"top center\" }}\n        initial={{ rotate: 0 }}\n        animate={swinging ? { rotate: BELL_SWING } : { rotate: 0 }}\n        transition={swingTransition}\n      >\n        <svg\n          viewBox=\"0 0 24 24\"\n          width={icon}\n          height={icon}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          aria-hidden=\"true\"\n        >\n          <path d={BELL_DOME_PATH} />\n          <motion.path\n            d={BELL_CLAPPER_PATH}\n            style={{ transformBox: \"fill-box\", transformOrigin: \"top center\" }}\n            initial={{ rotate: 0 }}\n            animate={swinging ? { rotate: CLAPPER_SWING } : { rotate: 0 }}\n            transition={swingTransition}\n          />\n        </svg>\n      </motion.span>\n\n      {dot ? (\n        <AnimatePresence initial={false}>\n          {count > 0 && (\n            <motion.span\n              key=\"dot\"\n              aria-hidden=\"true\"\n              className=\"absolute right-1 top-1 flex h-2.5 w-2.5\"\n              initial={{ scale: 0 }}\n              animate={{ scale: 1 }}\n              exit={{ scale: 0 }}\n              transition={badgeTransition}\n            >\n              {/* Re-keyed by ringKey so the halo pings exactly once per increase */}\n              {swinging && (\n                <motion.span\n                  key={`ping-${ringKey}`}\n                  className=\"absolute inset-0 rounded-full bg-rose-500\"\n                  initial={{ scale: 1, opacity: 0.6 }}\n                  animate={{ scale: 2, opacity: 0 }}\n                  transition={{ duration: PING_DURATION, ease: \"easeOut\" }}\n                />\n              )}\n              <span className=\"relative h-2.5 w-2.5 rounded-full bg-rose-500\" />\n            </motion.span>\n          )}\n        </AnimatePresence>\n      ) : (\n        <AnimatePresence initial={false}>\n          {count > 0 && (\n            <motion.span\n              key=\"badge\"\n              aria-hidden=\"true\"\n              className=\"absolute -right-1 -top-1 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-rose-500 px-1 text-[10px] font-semibold leading-none text-white\"\n              // Grow outward from where the badge attaches to the bell\n              style={{ transformOrigin: \"left bottom\" }}\n              initial={{ scale: 0 }}\n              animate={{ scale: 1 }}\n              exit={{ scale: 0 }}\n              transition={badgeTransition}\n            >\n              <span className=\"relative inline-flex overflow-hidden tabular-nums\">\n                <AnimatePresence\n                  mode=\"popLayout\"\n                  initial={false}\n                  custom={direction}\n                >\n                  <motion.span\n                    key={displayValue}\n                    className=\"inline-block\"\n                    custom={direction}\n                    variants={countVariants}\n                    initial=\"enter\"\n                    animate=\"center\"\n                    exit=\"exit\"\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : COUNT_SPRING\n                    }\n                  >\n                    {displayValue}\n                  </motion.span>\n                </AnimatePresence>\n              </span>\n            </motion.span>\n          )}\n        </AnimatePresence>\n      )}\n\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {count > 0\n          ? `${count} unread notification${count === 1 ? \"\" : \"s\"}`\n          : \"\"}\n      </span>\n    </motion.button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/notification-bell.tsx"
    }
  ],
  "type": "registry:component"
}
