{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-29",
  "title": "Notices in their tone",
  "description": "Success, error and info notices washed in their own hue, each dismissed with its space closing.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/status-orb.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/notification/notification-29.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  ArrowUpRightIcon,\n  RotateCcwIcon,\n  XIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { StatusOrb, type StatusOrbTone } from '@/registry/ndeko/ui/status-orb';\n\nimport { rowCollapseOf } from './_parts/collapse';\n\ntype ToneCard = {\n  id: string;\n  tone: StatusOrbTone;\n  color: string;\n  title: string;\n  description: string;\n  action: string;\n};\n\nconst CARDS: ToneCard[] = [\n  {\n    id: 'paid',\n    tone: 'success',\n    color: 'var(--success)',\n    title: 'Invoice 2041 is paid',\n    description:\n      'Fernhall Studio settled the March invoice. The receipt is in Billing.',\n    action: 'View receipt',\n  },\n  {\n    id: 'sync',\n    tone: 'error',\n    color: 'var(--destructive)',\n    title: 'Calendar sync stopped',\n    description:\n      'The connection to your work calendar expired. Plans after Friday are not imported.',\n    action: 'Reconnect calendar',\n  },\n  {\n    id: 'sign-in',\n    tone: 'info',\n    color: 'var(--info)',\n    title: 'New sign-in on a laptop',\n    description:\n      'Someone signed in from Lisbon at 08:12. If it was not you, end that session.',\n    action: 'Review sessions',\n  },\n];\n\nfunction ToneNotice({\n  card,\n  onDismiss,\n}: {\n  card: ToneCard;\n  onDismiss: (cardId: string) => void;\n}) {\n  const toneStyle = { '--tone': card.color } as React.CSSProperties;\n\n  return (\n    <div\n      role='status'\n      style={toneStyle}\n      className='bg-card hairline-ring relative isolate flex items-start gap-3 overflow-hidden rounded-2xl p-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)] ring-[color-mix(in_oklab,var(--tone)_28%,var(--border))]'\n    >\n      <span\n        aria-hidden='true'\n        className='pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(120%_140%_at_0%_0%,color-mix(in_oklab,var(--tone)_11%,transparent),transparent_55%)]'\n      />\n      <StatusOrb tone={card.tone} />\n      <div className='flex min-w-0 flex-1 flex-col gap-2 pt-1'>\n        <div className='flex flex-col gap-0.5 text-[13px] leading-5'>\n          <p className='font-medium'>{card.title}</p>\n          <p className='text-muted-foreground'>{card.description}</p>\n        </div>\n        <div>\n          <Button variant='link' size='sm' className='h-auto px-0 text-[13px]'>\n            {card.action}\n            <ArrowUpRightIcon />\n          </Button>\n        </div>\n      </div>\n      <Button\n        variant='ghost'\n        size='icon-sm'\n        aria-label={`Dismiss: ${card.title}`}\n        onClick={() => onDismiss(card.id)}\n        className='text-muted-foreground -me-1.5 -mt-1.5'\n      >\n        <XIcon />\n      </Button>\n    </div>\n  );\n}\n\nexport default function Notification29() {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const [cards, setCards] = useState(CARDS);\n  const isEmpty = cards.length === 0;\n  const collapse = rowCollapseOf(shouldReduceMotion);\n\n  function dismiss(cardId: string) {\n    setCards((current) => current.filter((card) => card.id !== cardId));\n  }\n\n  function restore() {\n    setCards(CARDS);\n  }\n\n  return (\n    <div className='-mx-1 -mt-1 -mb-2 flex w-full max-w-[26rem] flex-col'>\n      <AnimatePresence initial={false}>\n        {cards.map((card) => (\n          <motion.div\n            key={card.id}\n            variants={collapse}\n            initial='hidden'\n            animate='shown'\n            exit='hidden'\n            className='overflow-hidden'\n          >\n            <div className='px-1 pt-1 pb-2'>\n              <ToneNotice card={card} onDismiss={dismiss} />\n            </div>\n          </motion.div>\n        ))}\n        {isEmpty && (\n          <motion.div\n            key='empty'\n            variants={collapse}\n            initial='hidden'\n            animate='shown'\n            exit='hidden'\n            className='overflow-hidden'\n          >\n            <div className='flex flex-col items-center gap-3 px-1 pt-6 pb-8 text-center'>\n              <p className='text-muted-foreground text-[13px]'>\n                Every notice is cleared\n              </p>\n              <Button size='sm' variant='outline' onClick={restore}>\n                <RotateCcwIcon />\n                Show them again\n              </Button>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/notification/notification-29.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"
    }
  ],
  "categories": [
    "components",
    "notification"
  ],
  "type": "registry:component"
}