{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-24",
  "title": "Activity grouped by day",
  "description": "Notifications under Today, Yesterday and Earlier, each one snoozed or muted in place, with snoozed ones brought back from the header.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/avatar.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/dropdown-menu.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/tooltip.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/notification/notification-24.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion, type Transition } from 'motion/react';\nimport { useId, useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  AlarmClockIcon,\n  BellIcon,\n  BellOffIcon,\n  InboxIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from '@/registry/ndeko/ui/dropdown-menu';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/registry/ndeko/ui/tooltip';\n\nimport { rowCollapseOf } from './_parts/collapse';\nimport { PEOPLE, type Person } from './_parts/people';\nimport { SenderAvatar, type NotificationKind } from './_parts/sender-avatar';\nimport { SlidingCount } from './_parts/sliding-count';\nimport { Swap } from './_parts/swap';\n\ntype DayGroup = 'today' | 'yesterday' | 'week';\n\ntype ActivityEntry = {\n  id: string;\n  day: DayGroup;\n  kind: NotificationKind;\n  sender: Person;\n  text: string;\n  project: string;\n  time: string;\n  isMuted: boolean;\n  snoozedUntil: string | null;\n};\n\nconst DAY_LABELS: Record<DayGroup, string> = {\n  today: 'Today',\n  yesterday: 'Yesterday',\n  week: 'Earlier this week',\n};\n\nconst DAY_ORDER: DayGroup[] = ['today', 'yesterday', 'week'];\n\nconst SNOOZE_OPTIONS = ['In 1 hour', 'Tomorrow morning', 'Next Monday'];\n\nconst ENTRIES: ActivityEntry[] = [\n  {\n    id: 'a1',\n    day: 'today',\n    kind: 'comment',\n    sender: PEOPLE.leo,\n    text: 'replied in Offline drafts for field notes',\n    project: 'Field App',\n    time: '10:42',\n    isMuted: false,\n    snoozedUntil: null,\n  },\n  {\n    id: 'a2',\n    day: 'today',\n    kind: 'mention',\n    sender: PEOPLE.sana,\n    text: 'mentioned you in Booking calendar skips a week',\n    project: 'Field App',\n    time: '09:15',\n    isMuted: false,\n    snoozedUntil: null,\n  },\n  {\n    id: 'a3',\n    day: 'yesterday',\n    kind: 'assigned',\n    sender: PEOPLE.mei,\n    text: 'assigned you Copy for the weekly digest',\n    project: 'Growth',\n    time: '16:30',\n    isMuted: false,\n    snoozedUntil: null,\n  },\n  {\n    id: 'a4',\n    day: 'yesterday',\n    kind: 'status',\n    sender: PEOPLE.tomas,\n    text: 'closed Rate limit the share links',\n    project: 'Platform',\n    time: '11:05',\n    isMuted: true,\n    snoozedUntil: null,\n  },\n  {\n    id: 'a5',\n    day: 'week',\n    kind: 'comment',\n    sender: PEOPLE.amara,\n    text: 'commented on Onboarding checklist copy',\n    project: 'Growth',\n    time: 'Mon',\n    isMuted: false,\n    snoozedUntil: null,\n  },\n];\n\nconst PILL_TRANSITION: Transition = {\n  duration: 0.15,\n  ease: [0.23, 1, 0.32, 1],\n};\n\nfunction pillScaleOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return 1;\n  }\n  return 0.95;\n}\n\nfunction countLabelOf(count: number) {\n  return String(count);\n}\n\nfunction snoozedLabelOf(count: number) {\n  if (count === 1) {\n    return '1 snoozed';\n  }\n  return `${count} snoozed`;\n}\n\nfunction snoozedSentenceOf(count: number) {\n  if (count === 1) {\n    return '1 notification is snoozed';\n  }\n  return `${count} notifications are snoozed`;\n}\n\nfunction bringBackLabelOf(count: number) {\n  if (count === 1) {\n    return 'Bring back 1 snoozed notification';\n  }\n  return `Bring back ${count} snoozed notifications`;\n}\n\nfunction muteLabelOf(entry: ActivityEntry) {\n  if (entry.isMuted) {\n    return 'Unmute thread';\n  }\n  return 'Mute thread';\n}\n\nfunction isAwake(entry: ActivityEntry) {\n  return entry.snoozedUntil === null;\n}\n\nfunction entriesOfDay(entries: ActivityEntry[], day: DayGroup) {\n  return entries.filter((entry) => entry.day === day);\n}\n\nfunction withMuteToggled(entries: ActivityEntry[], entryId: string) {\n  return entries.map((entry) => {\n    if (entry.id !== entryId) {\n      return entry;\n    }\n    return { ...entry, isMuted: !entry.isMuted };\n  });\n}\n\nfunction withSnooze(entries: ActivityEntry[], entryId: string, until: string) {\n  return entries.map((entry) => {\n    if (entry.id !== entryId) {\n      return entry;\n    }\n    return { ...entry, snoozedUntil: until };\n  });\n}\n\nfunction allAwake(entries: ActivityEntry[]) {\n  return entries.map((entry) => ({ ...entry, snoozedUntil: null }));\n}\n\nconst ROW_ACTION =\n  'inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 hover:bg-foreground/5 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/25 data-popup-open:bg-foreground/5 data-popup-open:text-foreground';\n\ntype RowProps = {\n  entry: ActivityEntry;\n  onSnooze: (entryId: string, until: string) => void;\n  onToggleMute: (entryId: string) => void;\n};\n\nfunction ActivityRow({ entry, onSnooze, onToggleMute }: RowProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const muteLabel = muteLabelOf(entry);\n\n  return (\n    <motion.li\n      variants={rowCollapseOf(shouldReduceMotion)}\n      initial='hidden'\n      animate='shown'\n      exit='hidden'\n      className='overflow-hidden'\n    >\n      <div className='flex items-center gap-3 px-4 py-2'>\n        <SenderAvatar\n          person={entry.sender}\n          kind={entry.kind}\n          className={cn(\n            'transition-opacity duration-200',\n            entry.isMuted && 'opacity-60'\n          )}\n        />\n        <div className='min-w-0 flex-1'>\n          <p\n            className={cn(\n              'truncate text-[13px] leading-5 transition-colors duration-200',\n              entry.isMuted && 'text-muted-foreground'\n            )}\n          >\n            <span className='font-medium'>{entry.sender.name}</span>{' '}\n            <span className='text-muted-foreground'>{entry.text}</span>\n          </p>\n          <p className='text-muted-foreground flex items-center gap-1.5 text-xs'>\n            <span className='tabular-nums'>{entry.time}</span>\n            <span aria-hidden='true'>·</span>\n            <span className='truncate'>{entry.project}</span>\n            {entry.isMuted && (\n              <span className='inline-flex items-center gap-1'>\n                <span aria-hidden='true'>·</span>\n                Muted\n              </span>\n            )}\n          </p>\n        </div>\n        <div className='flex shrink-0 items-center gap-0.5'>\n          <DropdownMenu>\n            <Tooltip>\n              <TooltipTrigger\n                render={\n                  <DropdownMenuTrigger\n                    aria-label={`Snooze ${entry.sender.name}'s notification`}\n                    className={ROW_ACTION}\n                  />\n                }\n              >\n                <AlarmClockIcon className='size-3.5' />\n              </TooltipTrigger>\n              <TooltipContent>Snooze</TooltipContent>\n            </Tooltip>\n            <DropdownMenuContent align='end' className='w-48'>\n              <DropdownMenuLabel>Snooze until</DropdownMenuLabel>\n              {SNOOZE_OPTIONS.map((option) => (\n                <DropdownMenuItem\n                  key={option}\n                  onClick={() => onSnooze(entry.id, option)}\n                >\n                  <AlarmClockIcon />\n                  {option}\n                </DropdownMenuItem>\n              ))}\n            </DropdownMenuContent>\n          </DropdownMenu>\n          <Tooltip>\n            <TooltipTrigger\n              aria-label={muteLabel}\n              aria-pressed={entry.isMuted}\n              onClick={() => onToggleMute(entry.id)}\n              className={ROW_ACTION}\n            >\n              <Swap swapKey={String(entry.isMuted)}>\n                {entry.isMuted && <BellOffIcon className='size-3.5' />}\n                {!entry.isMuted && <BellIcon className='size-3.5' />}\n              </Swap>\n            </TooltipTrigger>\n            <TooltipContent>{muteLabel}</TooltipContent>\n          </Tooltip>\n        </div>\n      </div>\n    </motion.li>\n  );\n}\n\ntype DaySectionProps = {\n  day: DayGroup;\n  entries: ActivityEntry[];\n  onSnooze: (entryId: string, until: string) => void;\n  onToggleMute: (entryId: string) => void;\n};\n\nfunction DaySection({ day, entries, onSnooze, onToggleMute }: DaySectionProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const headingId = useId();\n\n  return (\n    <motion.section\n      aria-labelledby={headingId}\n      variants={rowCollapseOf(shouldReduceMotion)}\n      initial='hidden'\n      animate='shown'\n      exit='hidden'\n      className='overflow-hidden'\n    >\n      <h4\n        id={headingId}\n        className='text-muted-foreground flex items-center gap-1.5 px-4 pt-3 pb-1 text-xs'\n      >\n        {DAY_LABELS[day]}\n        <SlidingCount\n          value={entries.length}\n          format={countLabelOf}\n          className='text-muted-foreground/75'\n        />\n      </h4>\n      <ul>\n        <AnimatePresence initial={false}>\n          {entries.map((entry) => (\n            <ActivityRow\n              key={entry.id}\n              entry={entry}\n              onSnooze={onSnooze}\n              onToggleMute={onToggleMute}\n            />\n          ))}\n        </AnimatePresence>\n      </ul>\n    </motion.section>\n  );\n}\n\nfunction CaughtUp({ snoozedCount }: { snoozedCount: number }) {\n  return (\n    <div className='flex flex-col items-center px-6 py-8 text-center'>\n      <span className='keycap-3d text-muted-foreground flex size-9 items-center justify-center rounded-lg'>\n        <InboxIcon />\n      </span>\n      <p className='mt-3 text-sm font-medium'>Nothing left to read</p>\n      <p className='text-muted-foreground mt-1 text-[13px] text-balance'>\n        {snoozedSentenceOf(snoozedCount)}, bring them back from the header\n      </p>\n    </div>\n  );\n}\n\nexport default function Notification24() {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const titleId = useId();\n  const [entries, setEntries] = useState(ENTRIES);\n  const awakeEntries = entries.filter(isAwake);\n  const snoozedCount = entries.length - awakeEntries.length;\n  const hasSnoozed = snoozedCount > 0;\n  const isCaughtUp = awakeEntries.length === 0;\n  const pillScale = pillScaleOf(shouldReduceMotion);\n  const days = DAY_ORDER.filter(\n    (day) => entriesOfDay(awakeEntries, day).length > 0\n  );\n\n  function snooze(entryId: string, until: string) {\n    setEntries((current) => withSnooze(current, entryId, until));\n  }\n\n  function toggleMute(entryId: string) {\n    setEntries((current) => withMuteToggled(current, entryId));\n  }\n\n  function wakeAll() {\n    setEntries(allAwake);\n  }\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      className='bg-card hairline-ring ring-border w-full max-w-[26rem] rounded-xl pb-2 shadow-[0_1px_2px_rgb(0_0_0/0.04)]'\n    >\n      <header className='flex h-11 items-center justify-between gap-2 ps-4 pe-2'>\n        <h3 id={titleId} className='text-[13px] font-semibold'>\n          Activity\n        </h3>\n        <AnimatePresence initial={false}>\n          {hasSnoozed && (\n            <motion.div\n              key='snoozed'\n              initial={{ opacity: 0, scale: pillScale }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: pillScale }}\n              transition={PILL_TRANSITION}\n              className='origin-right'\n            >\n              <Button\n                variant='ghost'\n                size='sm'\n                aria-label={bringBackLabelOf(snoozedCount)}\n                onClick={wakeAll}\n                className='text-muted-foreground'\n              >\n                <AlarmClockIcon />\n                <SlidingCount value={snoozedCount} format={snoozedLabelOf} />\n                <span aria-hidden='true'>·</span>\n                <span className='text-foreground'>Bring back</span>\n              </Button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </header>\n      <AnimatePresence initial={false}>\n        {days.map((day) => (\n          <DaySection\n            key={day}\n            day={day}\n            entries={entriesOfDay(awakeEntries, day)}\n            onSnooze={snooze}\n            onToggleMute={toggleMute}\n          />\n        ))}\n        {isCaughtUp && (\n          <motion.div\n            key='caught-up'\n            variants={rowCollapseOf(shouldReduceMotion)}\n            initial='hidden'\n            animate='shown'\n            exit='hidden'\n            className='overflow-hidden'\n          >\n            <CaughtUp snoozedCount={snoozedCount} />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/notification/notification-24.tsx"
    },
    {
      "path": "registry/ndeko/components/notification/_parts/collapse.ts",
      "content": "import type { Transition, Variants } from 'motion/react';\n\nconst EASE_OUT_STRONG: [number, number, number, number] = [0.23, 1, 0.32, 1];\n\n/**\n * The spring AnimatedSize resizes with. It starts from rest, so the fade leads and the freed space\n * closes after it at the pace the surrounding container follows, instead of snapping shut.\n */\nconst SIZE_SPRING: Transition = { type: 'spring', bounce: 0, duration: 0.32 };\n\nconst ARRIVE: Transition = {\n  ...SIZE_SPRING,\n  opacity: { duration: 0.2, ease: EASE_OUT_STRONG },\n};\n\nconst LEAVE: Transition = {\n  ...SIZE_SPRING,\n  opacity: { duration: 0.12, ease: EASE_OUT_STRONG },\n};\n\nconst ARRIVE_FADE: Transition = { duration: 0.2, ease: EASE_OUT_STRONG };\n\nconst LEAVE_FADE: Transition = { duration: 0.12, ease: EASE_OUT_STRONG };\n\n/** A row whose spacing sits inside it: only its height and opacity change. */\nconst ROW_VARIANTS: Variants = {\n  shown: { height: 'auto', opacity: 1, transition: ARRIVE },\n  hidden: { height: 0, opacity: 0, transition: LEAVE },\n};\n\n/** A padded surface dismissed for good: its padding closes with its height. */\nconst SURFACE_VARIANTS: Variants = {\n  shown: { height: 'auto', opacity: 1, transition: ARRIVE },\n  hidden: {\n    height: 0,\n    opacity: 0,\n    paddingTop: 0,\n    paddingBottom: 0,\n    transition: LEAVE,\n  },\n};\n\nconst FADE_VARIANTS: Variants = {\n  shown: { opacity: 1, transition: ARRIVE_FADE },\n  hidden: { opacity: 0, transition: LEAVE_FADE },\n};\n\nexport function rowCollapseOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return ROW_VARIANTS;\n}\n\nexport function surfaceCollapseOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SURFACE_VARIANTS;\n}\n",
      "type": "registry:component",
      "target": "components/notification/_parts/collapse.ts"
    },
    {
      "path": "registry/ndeko/components/notification/_parts/people.ts",
      "content": "export type Person = {\n  id: string;\n  name: string;\n};\n\nexport const PEOPLE = {\n  amara: {\n    id: 'amara',\n    name: 'Amara Okafor',\n  },\n  leo: {\n    id: 'leo',\n    name: 'Léo Martin',\n  },\n  sana: {\n    id: 'sana',\n    name: 'Sana Iqbal',\n  },\n  tomas: {\n    id: 'tomas',\n    name: 'Tomás Rivera',\n  },\n  mei: {\n    id: 'mei',\n    name: 'Mei Lin',\n  },\n  jonas: { id: 'jonas', name: 'Jonas Weber' },\n} satisfies Record<string, Person>;\n\nexport function firstNameOf(person: Person) {\n  const [firstName] = person.name.split(' ');\n  return firstName;\n}\n",
      "type": "registry:component",
      "target": "components/notification/_parts/people.ts"
    },
    {
      "path": "registry/ndeko/components/notification/_parts/sender-avatar.tsx",
      "content": "import {\n  AtSignIcon,\n  CircleDashedIcon,\n  MailIcon,\n  MessageCircleIcon,\n  UserRoundPlusIcon,\n  type Icon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Avatar } from '@/registry/ndeko/ui/avatar';\n\nimport type { Person } from './people';\n\nexport type NotificationKind =\n  'mention' | 'comment' | 'assigned' | 'status' | 'invite';\n\ntype KindMeta = { label: string; color: string; glyph: Icon };\n\nexport const KIND_META: Record<NotificationKind, KindMeta> = {\n  mention: { label: 'Mention', color: 'var(--info)', glyph: AtSignIcon },\n  comment: {\n    label: 'Comment',\n    color: 'var(--success)',\n    glyph: MessageCircleIcon,\n  },\n  assigned: {\n    label: 'Assignment',\n    color: 'var(--primary)',\n    glyph: UserRoundPlusIcon,\n  },\n  status: {\n    label: 'Status change',\n    color: 'var(--muted-foreground)',\n    glyph: CircleDashedIcon,\n  },\n  invite: { label: 'Invitation', color: 'var(--warning)', glyph: MailIcon },\n};\n\ntype SenderAvatarProps = {\n  person: Person;\n  kind: NotificationKind;\n  /** The colour behind the badge's cut-out: the surface the avatar sits on. */\n  surfaceClassName?: string;\n  className?: string;\n};\n\n/** A sender's face with the kind of the notification on its lower corner. */\nexport function SenderAvatar({\n  person,\n  kind,\n  surfaceClassName = 'bg-card',\n  className,\n}: SenderAvatarProps) {\n  const meta = KIND_META[kind];\n  const Glyph = meta.glyph;\n  const badgeStyle = { '--chip': meta.color } as React.CSSProperties;\n\n  return (\n    <span className={cn('relative inline-flex shrink-0', className)}>\n      <Avatar className='size-8' seed={person.name} />\n      <span\n        aria-hidden='true'\n        className={cn(\n          'absolute -end-1 -bottom-1 inline-flex rounded-full p-px',\n          surfaceClassName\n        )}\n      >\n        <span\n          style={badgeStyle}\n          className='chip-3d inline-flex size-4 items-center justify-center rounded-full'\n        >\n          <Glyph strokeWidth={2.25} className='size-2.5' />\n        </span>\n      </span>\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/notification/_parts/sender-avatar.tsx"
    },
    {
      "path": "registry/ndeko/components/notification/_parts/sliding-count.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nconst SHIFT_PX = 8;\n\nconst SLIDE_TRANSITION: Transition = { duration: 0.18, ease: [0.2, 0, 0, 1] };\n\nconst SLIDE_VARIANTS: Variants = {\n  enter: (direction: number) => {\n    const offset = direction * SHIFT_PX;\n    return { opacity: 0, y: offset };\n  },\n  visible: { opacity: 1, y: 0 },\n  exit: (direction: number) => {\n    const offset = direction * SHIFT_PX;\n    return { opacity: 0, y: -offset };\n  },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction variantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SLIDE_VARIANTS;\n}\n\nfunction directionOf(previous: number, next: number) {\n  if (next > previous) {\n    return 1;\n  }\n  return -1;\n}\n\ntype Shown = { value: number; direction: number };\n\ntype SlidingCountProps = {\n  value: number;\n  /** Writes the figure in words, with its singular branch. */\n  format: (value: number) => string;\n  className?: string;\n};\n\n/** A figure the person just changed: it slides in the direction of the change. */\nexport function SlidingCount({ value, format, className }: SlidingCountProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const [shown, setShown] = useState<Shown>({ value, direction: 1 });\n  const text = format(value);\n\n  if (shown.value !== value) {\n    const direction = directionOf(shown.value, value);\n    setShown({ value, direction });\n  }\n\n  return (\n    <span\n      className={cn(\n        'relative inline-flex overflow-hidden tabular-nums',\n        className\n      )}\n    >\n      <AnimatePresence\n        mode='popLayout'\n        initial={false}\n        custom={shown.direction}\n      >\n        <motion.span\n          key={text}\n          custom={shown.direction}\n          variants={variantsOf(shouldReduceMotion)}\n          initial='enter'\n          animate='visible'\n          exit='exit'\n          transition={SLIDE_TRANSITION}\n          className='whitespace-nowrap'\n        >\n          {text}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/notification/_parts/sliding-count.tsx"
    },
    {
      "path": "registry/ndeko/components/notification/_parts/swap.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: [0.2, 0, 0, 1] };\n\nconst SWAP_VARIANTS: Variants = {\n  enter: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  visible: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction variantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SWAP_VARIANTS;\n}\n\ntype SwapProps = {\n  /** Changes when the content becomes another one. */\n  swapKey: string;\n  className?: string;\n  children: React.ReactNode;\n};\n\n/** An icon that becomes another: the old one shrinks out of focus while the new one settles. */\nexport function Swap({ swapKey, className, children }: SwapProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n\n  return (\n    <AnimatePresence mode='popLayout' initial={false}>\n      <motion.span\n        key={swapKey}\n        variants={variantsOf(shouldReduceMotion)}\n        initial='enter'\n        animate='visible'\n        exit='exit'\n        transition={SWAP_TRANSITION}\n        className={cn('flex', className)}\n      >\n        {children}\n      </motion.span>\n    </AnimatePresence>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/notification/_parts/swap.tsx"
    }
  ],
  "categories": [
    "components",
    "notification"
  ],
  "type": "registry:component"
}