{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reminders-card",
  "type": "registry:ui",
  "title": "Reminders Card",
  "description": "A widget-style checklist with an accent header, live clock, animated checkboxes and inline add.",
  "files": [
    {
      "path": "src/registry/ui/reminders-card.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useId, useRef, useState, useSyncExternalStore } from \"react\";\nimport { Check, Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type Reminder = { id: string; label: string; done: boolean };\n\nexport type RemindersCardProps = Omit<\n  React.ComponentProps<\"section\">,\n  \"title\"\n> & {\n  title?: React.ReactNode;\n  listTitle?: React.ReactNode;\n  /** Reminders (controlled). */\n  reminders?: Reminder[];\n  defaultReminders?: Reminder[];\n  onRemindersChange?: (reminders: Reminder[]) => void;\n  /** IANA time zone for the header clock. Defaults to the visitor's own. */\n  timeZone?: string;\n  /** Hide the header clock. */\n  showClock?: boolean;\n  /** Called by \"Done\" when no reminder is being added. */\n  onDone?: () => void;\n  doneLabel?: React.ReactNode;\n  placeholder?: string;\n  /** Any CSS color. Tints the header gradient and the card. */\n  accent?: string;\n};\n\nconst EASE_OUT = \"ease-[cubic-bezier(0.23,1,0.32,1)]\";\n\n// Soft light spots over the accent, like a lit glass widget.\nconst HEADER_GLOW = [\n  \"radial-gradient(120% 140% at 20% -10%, color-mix(in oklab, var(--reminders-accent) 35%, white), transparent 55%)\",\n  \"radial-gradient(90% 120% at 105% 110%, color-mix(in oklab, var(--reminders-accent) 45%, white), transparent 50%)\",\n  \"var(--reminders-accent)\",\n].join(\", \");\n\nlet nextId = 0;\nconst createId = () => `reminder-${Date.now().toString(36)}-${nextId++}`;\n\nexport function RemindersCard({\n  title = \"Reminders\",\n  listTitle = \"Tasks for Today\",\n  reminders,\n  defaultReminders = [],\n  onRemindersChange,\n  timeZone,\n  showClock = true,\n  onDone,\n  doneLabel = \"Done\",\n  placeholder = \"New reminder\",\n  accent = \"#0A6EFF\",\n  className,\n  style,\n  ...props\n}: RemindersCardProps) {\n  const listTitleId = useId();\n  const inputId = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const time = useClock(showClock ? (timeZone ?? \"local\") : undefined);\n\n  const [uncontrolled, setUncontrolled] = useState(defaultReminders);\n  const items = reminders ?? uncontrolled;\n  const update = (next: Reminder[]) => {\n    if (reminders === undefined) setUncontrolled(next);\n    onRemindersChange?.(next);\n  };\n\n  const [adding, setAdding] = useState(false);\n  const [draft, setDraft] = useState(\"\");\n\n  const toggle = (id: string) =>\n    update(\n      items.map((item) =>\n        item.id === id ? { ...item, done: !item.done } : item,\n      ),\n    );\n\n  // Adds the draft and keeps the field open for the next one, like Reminders on iOS.\n  const commit = () => {\n    const label = draft.trim();\n    if (label) update([...items, { id: createId(), label, done: false }]);\n    setDraft(\"\");\n  };\n  // Done and × both keep what was typed, so closing never loses a reminder.\n  const finishAdding = () => {\n    commit();\n    setAdding(false);\n  };\n\n  const openAdding = () => {\n    setAdding(true);\n    // The input mounts on this render; focus it right after.\n    requestAnimationFrame(() => inputRef.current?.focus());\n  };\n\n  return (\n    <section\n      data-slot=\"reminders-card\"\n      aria-labelledby={listTitleId}\n      style={\n        {\n          \"--reminders-accent\": accent,\n          \"--reminders-paper\": `color-mix(in oklab, ${accent} 5%, white)`,\n          \"--reminders-ink\": `color-mix(in oklab, ${accent} 22%, #0b0f19)`,\n          ...style,\n        } as React.CSSProperties\n      }\n      className={cn(\n        \"w-full max-w-md overflow-hidden rounded-[2.5rem] text-[var(--reminders-ink)] shadow-2xl shadow-black/10 \",\n        className,\n      )}\n      {...props}\n    >\n      <header\n        style={{ background: HEADER_GLOW }}\n        className=\"flex items-start justify-between gap-4 px-8 pt-7 pb-14 text-white \"\n      >\n        <div>\n          <h3 className=\"text-lg leading-tight font-semibold\">{title}</h3>\n          {showClock && (\n            <p className=\"text-base font-medium text-white/70 tabular-nums\">\n              <span className=\"sr-only\">Current time </span>\n              {/* Empty until mounted: the server can't know the visitor's clock. */}\n              {time ?? \" \"}\n            </p>\n          )}\n        </div>\n        <button\n          type=\"button\"\n          onClick={adding ? finishAdding : onDone}\n          className={cn(\n            \"inline-flex h-10 cursor-pointer items-center rounded-full bg-white/25 px-4 text-sm font-semibold text-white ring-1 ring-white/30 backdrop-blur-sm outline-none\",\n            \"transition-[background-color,transform] duration-[160ms]\",\n            EASE_OUT,\n            \"hover:bg-white/35 focus-visible:ring-2 focus-visible:ring-white active:scale-[0.97] motion-reduce:active:scale-100\",\n          )}\n        >\n          {doneLabel}\n        </button>\n      </header>\n\n      <div className=\"relative -mt-8 rounded-t-[2.5rem] bg-[var(--reminders-paper)] px-8 pt-8 pb-8\">\n        <h4 id={listTitleId} className=\"text-3xl font-bold tracking-[-0.03em]\">\n          {listTitle}\n        </h4>\n\n        <ul className=\"mt-5 flex flex-col gap-3.5 pr-16\">\n          {items.map((item) => (\n            <li\n              key={item.id}\n              className={cn(\n                \"transition-[opacity,transform] duration-200 starting:opacity-0 starting:translate-y-1 motion-reduce:starting:translate-y-0\",\n                EASE_OUT,\n              )}\n            >\n              <ReminderCheckbox item={item} onToggle={() => toggle(item.id)} />\n            </li>\n          ))}\n\n          {adding && (\n            <li\n              className={cn(\n                \"flex items-center gap-3 transition-[opacity,transform] duration-200 starting:opacity-0 starting:translate-y-1 motion-reduce:starting:translate-y-0\",\n                EASE_OUT,\n              )}\n            >\n              <span\n                aria-hidden\n                className=\"size-6 shrink-0 rounded-full border-[1.5px] border-[var(--reminders-ink)] opacity-40\"\n              />\n              <label htmlFor={inputId} className=\"sr-only\">\n                {placeholder}\n              </label>\n              <input\n                ref={inputRef}\n                id={inputId}\n                value={draft}\n                onChange={(event) => setDraft(event.target.value)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Enter\") {\n                    event.preventDefault();\n                    commit();\n                  } else if (event.key === \"Escape\") {\n                    // Escape cancels: drop the draft.\n                    setDraft(\"\");\n                    setAdding(false);\n                  }\n                }}\n                placeholder={placeholder}\n                enterKeyHint=\"done\"\n                autoComplete=\"off\"\n                className=\"min-w-0 flex-1 bg-transparent text-lg outline-none placeholder:text-current placeholder:opacity-40\"\n              />\n            </li>\n          )}\n        </ul>\n\n        <button\n          type=\"button\"\n          onClick={adding ? finishAdding : openAdding}\n          aria-label={adding ? \"Finish adding reminders\" : \"Add reminder\"}\n          aria-expanded={adding}\n          aria-controls={adding ? inputId : undefined}\n          className={cn(\n            \"absolute right-7 bottom-7 flex size-16 cursor-pointer items-center justify-center rounded-full bg-[var(--reminders-paper)] outline-none\",\n            \"shadow-[0_0_0_1px_color-mix(in_oklab,var(--reminders-accent)_25%,transparent),0_8px_20px_-8px_color-mix(in_oklab,var(--reminders-accent)_45%,transparent)]\",\n            \"transition-[background-color,transform] duration-[160ms]\",\n            EASE_OUT,\n            \"hover:bg-white focus-visible:ring-2 focus-visible:ring-[var(--reminders-accent)] active:scale-[0.97] motion-reduce:active:scale-100\",\n          )}\n        >\n          {/* + turns into × while adding. */}\n          <Plus\n            aria-hidden\n            className={cn(\n              \"size-7 stroke-[1.75] transition-transform duration-200\",\n              EASE_OUT,\n              adding && \"rotate-45\",\n            )}\n          />\n        </button>\n      </div>\n    </section>\n  );\n}\n\nfunction ReminderCheckbox({\n  item,\n  onToggle,\n}: {\n  item: Reminder;\n  onToggle: () => void;\n}) {\n  return (\n    <label className=\"group flex cursor-pointer items-start gap-3 text-lg\">\n      <input\n        type=\"checkbox\"\n        checked={item.done}\n        onChange={onToggle}\n        className=\"peer sr-only\"\n      />\n      <span\n        aria-hidden\n        className={cn(\n          \"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border-[1.5px] border-[var(--reminders-ink)] text-[var(--reminders-paper)]\",\n          \"transition-[background-color,border-color,opacity,transform] duration-150\",\n          EASE_OUT,\n          \"opacity-40 group-hover:opacity-60 group-active:scale-[0.92] motion-reduce:group-active:scale-100\",\n          \"peer-checked:bg-[var(--reminders-ink)] peer-checked:opacity-100\",\n          \"peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--reminders-accent)] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[var(--reminders-paper)]\",\n          // The check fades in from slightly smaller, never from nothing.\n          \"[&>svg]:scale-90 [&>svg]:opacity-0 [&>svg]:transition-[opacity,transform] [&>svg]:duration-150 peer-checked:[&>svg]:scale-100 peer-checked:[&>svg]:opacity-100\",\n        )}\n      >\n        <Check className=\"size-3.5 stroke-[3]\" />\n      </span>\n      {/* Done reminders read darker in this design; open ones are muted. */}\n      <span className=\"opacity-55 transition-opacity duration-150 peer-checked:opacity-100\">\n        {item.label}\n      </span>\n    </label>\n  );\n}\n\n// \"14:17\" in a time zone (\"local\" for the visitor's), updating on each minute boundary.\nfunction useClock(timeZone: string | undefined) {\n  return useSyncExternalStore(\n    subscribeToMinutes,\n    () => (timeZone ? formatClock(timeZone) : null),\n    () => null,\n  );\n}\n\nfunction subscribeToMinutes(onTick: () => void) {\n  let interval: ReturnType<typeof setInterval> | undefined;\n  const timeout = setTimeout(\n    () => {\n      onTick();\n      interval = setInterval(onTick, 60_000);\n    },\n    60_000 - (Date.now() % 60_000),\n  );\n  return () => {\n    clearTimeout(timeout);\n    clearInterval(interval);\n  };\n}\n\nconst clockFormatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction formatClock(timeZone: string) {\n  let formatter = clockFormatters.get(timeZone);\n  if (!formatter) {\n    formatter = new Intl.DateTimeFormat(\"en-GB\", {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      hourCycle: \"h23\",\n      timeZone: timeZone === \"local\" ? undefined : timeZone,\n    });\n    clockFormatters.set(timeZone, formatter);\n  }\n  return formatter.format(new Date());\n}\n"
    }
  ],
  "dependencies": [
    "lucide-react"
  ]
}
