{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "due-date-slip",
  "title": "Due-date slip",
  "description": "The library card in the back of a book: each loan or renewal stamps the next ruled line, and the last date is when it's due. Overdue dates are stamped red. For expiring keys, trials, seats and access grants.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json",
    "https://bevel-ui.vercel.app/r/stamp.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/due-date-slip.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { cn } from \"cn\"\nimport { Stamp } from \"@/components/ui/stamp\"\n\nconst DAY = 86_400_000\n\n/** `now` is undefined until the slip knows today: before mount, when no `now` prop was given. */\ntype DueDateSlipContextValue = { latest: Date | undefined; now: Date | undefined; statusId: string }\n\nconst DueDateSlipContext = React.createContext<DueDateSlipContextValue | null>(null)\n\nfunction useDueDateSlip() {\n  const context = React.useContext(DueDateSlipContext)\n  if (!context) throw new Error(\"DueDateSlip parts must be inside <DueDateSlip>\")\n  return context\n}\n\nfunction toDate(d: Date | string) {\n  return typeof d === \"string\" ? new Date(d) : d\n}\n\n/** Fixed locale and time zone, so server and client stamp the same date. */\nfunction stampText(d: Date) {\n  return d\n    .toLocaleDateString(\"en-GB\", { day: \"2-digit\", month: \"short\", year: \"numeric\", timeZone: \"UTC\" })\n    .toUpperCase()\n}\n\ntype DueDateSlipProps = React.ComponentProps<\"div\"> & {\n  /** What's on loan: a key, a seat, a trial. Printed at the top. */\n  title: string\n  /** Every due date stamped so far, oldest first. The last one counts. */\n  dates: (Date | string)[]\n  /** Today. Pass it to keep server and client in step. */\n  now?: Date | string\n}\n\n/**\n * A library date-due slip. Each loan or renewal stamps the next line;\n * the last date is when it's due. Overdue dates are stamped in red.\n */\nfunction DueDateSlip({ className, title, dates, now: nowProp, children, ...props }: DueDateSlipProps) {\n  const statusId = React.useId()\n  // Read the clock after mount, not during render, so a prerendered page and\n  // the browser agree. Pass `now` to know it from the first render.\n  const [clientNow, setClientNow] = React.useState<Date>()\n  React.useEffect(() => {\n    if (nowProp === undefined) setClientNow(new Date())\n  }, [nowProp])\n  const now = React.useMemo(() => (nowProp === undefined ? clientNow : toDate(nowProp)), [nowProp, clientNow])\n  const parsed = dates.map(toDate)\n  const latest = parsed.at(-1)\n  // Stamp only dates added after the slip appeared, not the history.\n  const initial = React.useRef(dates.length)\n  return (\n    <DueDateSlipContext value={{ latest, now, statusId }}>\n      <div\n        role=\"group\"\n        aria-label={`${title}: date due`}\n        data-slot=\"due-date-slip\"\n        data-overdue={latest && now && latest < now ? \"\" : undefined}\n        className={cn(\"bevel-paper flex w-64 flex-col rounded-(--bevel-radius-xs) px-4 pt-3 pb-4\", className)}\n        {...props}\n      >\n        <p className=\"truncate text-xs font-semibold\">{title}</p>\n        <p\n          aria-hidden\n          className=\"mt-1 border-b-2 border-(--due-date-slip-header) pb-1 text-center text-sm font-black tracking-(--bevel-tracking-label) text-(--due-date-slip-header)\"\n        >\n          DATE DUE\n        </p>\n        <ol\n          aria-label=\"Due dates\"\n          className=\"mb-3 flex flex-col bg-[linear-gradient(to_bottom,transparent_calc(var(--due-date-slip-line)-1px),var(--due-date-slip-rule)_calc(var(--due-date-slip-line)-1px))] bg-size-[100%_var(--due-date-slip-line)]\"\n        >\n          {parsed.map((d, i) => {\n            const last = i === parsed.length - 1\n            return (\n              <li\n                key={`${d.getTime()}-${i}`}\n                className={cn(\"flex h-(--due-date-slip-line) items-center justify-center\", !last && \"opacity-60\")}\n              >\n                <Stamp tone={now && d < now ? \"red\" : \"blue\"} animate={i >= initial.current} className=\"py-0.5\">\n                  {stampText(d)}\n                </Stamp>\n              </li>\n            )\n          })}\n          {/* Empty lines below, waiting for renewals. */}\n          {Array.from({ length: Math.max(0, 4 - parsed.length) }, (_, i) => (\n            <li key={`empty-${i}`} aria-hidden className=\"h-(--due-date-slip-line)\" />\n          ))}\n        </ol>\n        {children}\n      </div>\n    </DueDateSlipContext>\n  )\n}\n\n/** When it's due, in words: “Due in 12 days” or “Overdue by 3 days”. Announced when it changes. */\nfunction DueDateSlipStatus({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { latest, now, statusId } = useDueDateSlip()\n  let text = \"Not on loan.\"\n  if (latest) {\n    const date = stampText(latest).toLowerCase().replace(/(^|\\s)\\S/g, (c) => c.toUpperCase())\n    const days = now ? Math.round((latest.getTime() - now.getTime()) / DAY) : undefined\n    text =\n      days === undefined\n        ? `Due ${date}.`\n        : days > 1\n        ? `Due ${date}, in ${days} days.`\n        : days === 1\n          ? `Due tomorrow, ${date}.`\n          : days === 0\n            ? `Due today.`\n            : `Overdue by ${-days} day${days === -1 ? \"\" : \"s\"}. Was due ${date}.`\n  }\n  return (\n    <p\n      id={statusId}\n      role=\"status\"\n      data-slot=\"due-date-slip-status\"\n      className={cn(\"text-xs in-data-overdue:font-semibold in-data-overdue:text-(--bevel-ink-red)\", className)}\n      {...props}\n    >\n      {text}\n    </p>\n  )\n}\n\n/** Renews the loan. Your handler adds the next date. */\nfunction DueDateSlipRenew({ className, ...props }: ButtonPrimitive.Props) {\n  const { statusId } = useDueDateSlip()\n  return (\n    <ButtonPrimitive\n      data-slot=\"due-date-slip-renew\"\n      aria-describedby={statusId}\n      className={cn(\n        \"bevel-raised mt-2 h-9 rounded-(--bevel-radius-sm) px-4 text-sm font-semibold text-(--bevel-paper-foreground) outline-none [--bevel-material:var(--bevel-paper)]\",\n        \"transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) active:translate-y-0.5 active:bevel-pressed motion-reduce:transition-none\",\n        \"focus-visible:ring-3 focus-visible:ring-ring/60 data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { DueDateSlip, DueDateSlipStatus, DueDateSlipRenew }\nexport type { DueDateSlipProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--due-date-slip-paper": "oklch(0.93 0.05 95)",
      "--due-date-slip-rule": "oklch(0.7 0.08 240 / 0.45)",
      "--due-date-slip-header": "oklch(0.45 0.12 27)",
      "--due-date-slip-line": "2rem"
    },
    "[data-slot=\"due-date-slip\"]": {
      "--bevel-paper": "var(--due-date-slip-paper)"
    }
  },
  "categories": [
    "records"
  ],
  "type": "registry:ui"
}