{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "event-reminders",
  "title": "Event reminders",
  "description": "Set when and how people are reminded of an event, by notification or email.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/event-reminders/event-reminders.tsx",
      "content": "// Based on Watermelon UI's “Event reminders” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten as a controlled list with labelled controls.\nimport { Bell, Mail, Minus, Plus, X } from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\nimport { useState } from \"react\";\nimport { RollingNumber } from \"@/registry/manniche/rolling-number/rolling-number\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ReminderChannel = \"notification\" | \"email\";\nexport type ReminderUnit = \"minutes\" | \"hours\" | \"days\";\nexport type Reminder = {\n  id: string;\n  channel: ReminderChannel;\n  amount: number;\n  unit: ReminderUnit;\n};\n\nexport type EventRemindersProps = {\n  title: string;\n  /** Already formatted, e.g. “Thursday 9 October, 14:00”. */\n  when: string;\n  /** Controlled list. Leave it out and the card keeps its own. */\n  value?: Reminder[];\n  defaultValue?: Reminder[];\n  onChange?: (reminders: Reminder[]) => void;\n  /** Most reminders one event can have. */\n  max?: number;\n  labels?: Partial<\n    Record<\n      | ReminderChannel\n      | ReminderUnit\n      | \"before\"\n      | \"add\"\n      | \"remove\"\n      | \"more\"\n      | \"fewer\",\n      string\n    >\n  >;\n  className?: string;\n};\n\nconst UNITS: ReminderUnit[] = [\"minutes\", \"hours\", \"days\"];\nconst LIMIT: Record<ReminderUnit, number> = {\n  minutes: 59,\n  hours: 23,\n  days: 30,\n};\nconst EN = {\n  notification: \"Notification\",\n  email: \"Email\",\n  minutes: \"minutes\",\n  hours: \"hours\",\n  days: \"days\",\n  before: \"before\",\n  add: \"Add reminder\",\n  remove: \"Remove reminder\",\n  more: \"More\",\n  fewer: \"Less\",\n};\n\n/** A card where people set when and how they are reminded of an event. */\nexport function EventReminders({\n  title,\n  when,\n  value,\n  defaultValue = [],\n  onChange,\n  max = 5,\n  labels = {},\n  className,\n}: EventRemindersProps) {\n  const [own, setOwn] = useState(defaultValue);\n  const list = value ?? own;\n  const t = { ...EN, ...labels };\n\n  const set = (next: Reminder[]) => {\n    setOwn(next);\n    onChange?.(next);\n  };\n  const patch = (id: string, p: Partial<Reminder>) =>\n    set(list.map((r) => (r.id === id ? { ...r, ...p } : r)));\n\n  const pill =\n    \"inline-flex min-h-10 items-center gap-1.5 rounded-full bg-muted px-3 text-sm font-medium transition-colors duration-150 hover:bg-accent\";\n  const round =\n    \"grid size-8 place-items-center rounded-full text-muted-foreground transition-colors duration-150 hover:bg-background hover:text-foreground disabled:opacity-30\";\n\n  return (\n    <MotionConfig\n      reducedMotion=\"user\"\n      transition={{ type: \"spring\", bounce: 0, duration: 0.45 }}\n    >\n      <motion.section\n        layout\n        className={cn(\n          \"w-full max-w-md rounded-3xl border bg-card p-5 text-card-foreground shadow-sm\",\n          className,\n        )}\n      >\n        <header className=\"pb-4\">\n          <h2 className=\"text-lg font-semibold text-balance\">{title}</h2>\n          <p className=\"mt-1 text-sm text-muted-foreground\">{when}</p>\n        </header>\n\n        <ul className=\"space-y-2 border-t border-dashed pt-3\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            {list.map((r) => {\n              const Icon = r.channel === \"email\" ? Mail : Bell;\n              return (\n                <motion.li\n                  key={r.id}\n                  layout\n                  initial={{ opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, scale: 0.96 }}\n                  className=\"flex items-start gap-1.5\"\n                >\n                  {/* Amount, unit and “before” wrap as one piece, so a narrow card never strands a word. */}\n                  <div className=\"flex flex-1 flex-wrap items-center gap-1.5\">\n                    <button\n                      type=\"button\"\n                      className={pill}\n                      onClick={() =>\n                        patch(r.id, {\n                          channel:\n                            r.channel === \"email\" ? \"notification\" : \"email\",\n                        })\n                      }\n                    >\n                      <Icon className=\"size-4\" aria-hidden />\n                      {t[r.channel]}\n                    </button>\n                    <span className=\"inline-flex items-center gap-1.5\">\n                      <span className=\"inline-flex min-h-10 items-center rounded-full bg-muted px-1\">\n                        <button\n                          type=\"button\"\n                          className={round}\n                          aria-label={t.fewer}\n                          disabled={r.amount <= 1}\n                          onClick={() => patch(r.id, { amount: r.amount - 1 })}\n                        >\n                          <Minus className=\"size-3.5\" aria-hidden />\n                        </button>\n                        <RollingNumber\n                          value={r.amount}\n                          className=\"min-w-6 justify-center text-sm font-semibold\"\n                        />\n                        <button\n                          type=\"button\"\n                          className={round}\n                          aria-label={t.more}\n                          disabled={r.amount >= LIMIT[r.unit]}\n                          onClick={() => patch(r.id, { amount: r.amount + 1 })}\n                        >\n                          <Plus className=\"size-3.5\" aria-hidden />\n                        </button>\n                      </span>\n                      <button\n                        type=\"button\"\n                        className={pill}\n                        onClick={() => {\n                          const unit =\n                            UNITS[(UNITS.indexOf(r.unit) + 1) % UNITS.length];\n                          patch(r.id, {\n                            unit,\n                            amount: Math.min(r.amount, LIMIT[unit]),\n                          });\n                        }}\n                      >\n                        {t[r.unit]}\n                      </button>\n                      <span className=\"px-1 text-sm text-muted-foreground\">\n                        {t.before}\n                      </span>\n                    </span>\n                  </div>\n                  <button\n                    type=\"button\"\n                    className={cn(round, \"size-10\")}\n                    aria-label={t.remove}\n                    onClick={() => set(list.filter((x) => x.id !== r.id))}\n                  >\n                    <X className=\"size-4\" aria-hidden />\n                  </button>\n                </motion.li>\n              );\n            })}\n          </AnimatePresence>\n        </ul>\n\n        {list.length < max && (\n          <motion.button\n            layout\n            type=\"button\"\n            onClick={() =>\n              set([\n                ...list,\n                {\n                  id: crypto.randomUUID(),\n                  channel: \"notification\",\n                  amount: 10,\n                  unit: \"minutes\",\n                },\n              ])\n            }\n            className=\"mt-3 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-2xl border border-dashed text-sm font-medium text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n          >\n            <Plus className=\"size-4\" aria-hidden />\n            {t.add}\n          </motion.button>\n        )}\n      </motion.section>\n    </MotionConfig>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/manniche/rolling-number/rolling-number.tsx",
      "content": "// Based on the rolling digits in Watermelon UI's “Stepper” and “Pagination”\n// (MIT, © 2026 Watermelon Platform Contributors, github.com/WatermelonCorp/watermelon-platform).\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useState } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type RollingNumberProps = {\n  value: number\n  /** Passed to Intl.NumberFormat, e.g. \"da-DK\". Leave it out for plain digits. */\n  locale?: string\n  className?: string\n}\n\n/** A number whose changed digits roll up or down. Screen readers get the whole number once. */\nexport function RollingNumber({ value, locale, className }: RollingNumberProps) {\n  // Remember the last value in state (not a ref), so the direction survives a double render.\n  const [last, setLast] = useState({ value, dir: 1 })\n  const dir = last.value === value ? last.dir : value > last.value ? 1 : -1\n  if (last.value !== value) setLast({ value, dir })\n  const text = locale ? new Intl.NumberFormat(locale).format(value) : String(value)\n  const chars = text.split('')\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <span className={cn('inline-flex tabular-nums', className)}>\n        <span className=\"sr-only\">{text}</span>\n        <span aria-hidden className=\"inline-flex\">\n          {chars.map((ch, i) => {\n            // Count from the right, so the ones stay the ones when the number grows a digit.\n            const place = chars.length - i\n            return (\n              <span key={place} className=\"relative inline-flex min-w-[1ch] justify-center overflow-hidden\">\n                <AnimatePresence mode=\"popLayout\" initial={false} custom={dir}>\n                  <motion.span\n                    key={ch}\n                    initial={{ y: `${dir * 70}%`, opacity: 0, filter: 'blur(2px)' }}\n                    animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}\n                    exit={{ y: `${dir * -70}%`, opacity: 0, filter: 'blur(2px)' }}\n                    transition={{ type: 'spring', stiffness: 260, damping: 32 }}\n                    className=\"inline-block\"\n                  >\n                    {ch}\n                  </motion.span>\n                </AnimatePresence>\n              </span>\n            )\n          })}\n        </span>\n      </span>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}