Components

Getting started

Components154

More

One date, typed or picked: the field reads "tomorrow", "next fri" or "4 nov", and the calendar slides by month while one mark glides between days.

Used forDue datesBookingsFiltersProfiles

Install with the shadcn CLI

$ npx shadcn@latest add https://hairlineui.com/r/date-picker.json

Templates

Need the whole page?

43 Next.js landing pages, each designed from scratch. Try any of them live; one is $39, all of them $99, and 5 are free.

All 43 templates

Where it’s used

  • Due dates

    Set a task for "next fri" without opening anything.

  • Bookings

    A delivery or appointment day with limits.

  • Filters

    A single "since" date above a report.

  • Profiles

    Start dates and birthdays.

What it handles for you

Calendars are slow for dates people already know, and typed fields are strict about format. This accepts both.

Instead of wiring upreact-day-pickerdate-fns parsingnative <input type="date">
  1. Natural typing

    today, tomorrow, weekdays, "in 3 days/weeks/months", "4 nov", "nov 4", 4/11/2026, ISO.

  2. Moving mark

    One selection circle thrown between days (420ms).

  3. Full keyboard

    Arrows by day/week, Page Up/Down by month, Home/End, Enter.

  4. No time-zone bugs

    Plain YYYY-MM-DD strings in and out.

Overview

Date fields usually make you click through a calendar or type an exact format. This one reads what people write ("tomorrow", "in 3 days", "next fri", "4 nov", "2026-11-04") and settles it into "Wed 4 Nov 2026"; anything it can't read shakes and says so. The calendar rises off the field, months slide in from the side you moved toward while the month name morphs, and the chosen day's mark glides from day to day. Values are plain "YYYY-MM-DD" strings, so time zones never shift a day.

States

typed
Enter or leaving the field settles the text into a formatted date.
unreadable
Red line, a hint of what works, one shake.
calendar
Rises off the field; months slide 24px out of a blur (360ms); the month name morphs.
pick
The mark glides to the day; the calendar closes and focus returns to the field.

Usage

usage.tsx
import { DatePicker } from "@/components/ui/date-picker";<DatePicker label="Due" value={due} onValueChange={setDue} min="2026-10-20" />

Works with

Props

PropTypeDescription
value / defaultValuestring | null"YYYY-MM-DD".
onValueChange(value: string | null) => voidCalled with the new day (null when cleared).
min / maxstringAllowed range.
weekStartsOn0 | 1default 1Sunday or Monday.
localestringdefault "en-GB"Formatting.
label / hint / error / placeholderReactNodeAs in Input.

Accessibility and motion

  • parseDate(input, today) is exported for your own fields.
  • Alt+↓ opens the calendar from the field; the grid uses role="grid" with labelled days.
  • Reduced motion: months and the mark change in place.

Requirements

React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

The CLI also installs Text morph, ui-interaction-motion.

Source

components/date-picker.tsx
"use client";import * as React from "react";import { createPortal } from "react-dom";import { Field, fieldSurface, useFieldIds, useShake } from "./field";import { enterStyle, themeVars, useDismiss, useFloating, usePresence } from "./floating";import { TextMorph } from "./text-morph";import { EASE_OUT, EASE_THROW, INTERACTION_FOCUS, INTERACTION_PRESS, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * DATE PICKER: one date, typed or picked * *   type     the field reads what people write: "tomorrow", *            "next fri", "in 3 days", "4 nov", "2026-11-04"; Enter *            or leaving the field settles it into "Wed 4 Nov 2026", *            and anything it can't read shakes and says so *   pick     the calendar rises off the field; months slide in from *            the side you moved toward while the month name morphs; *            the chosen day's mark glides from day to day *   keys     arrows move a day or a week, Page Up/Down a month, *            Home/End the week's ends, Enter picks, Escape closes * * Values are plain "YYYY-MM-DD" strings: no time zones to trip on. * ───────────────────────────────────────────────────────── */export interface DatePickerProps {  value?: string | null;  defaultValue?: string | null;  onValueChange?: (value: string | null) => void;  label?: React.ReactNode;  hint?: React.ReactNode;  error?: React.ReactNode;  placeholder?: string;  /** Earliest and latest allowed days, "YYYY-MM-DD". */  min?: string;  max?: string;  /** 1 = Monday (default), 0 = Sunday. */  weekStartsOn?: 0 | 1;  locale?: string;  id?: string;  disabled?: boolean;  className?: string;}// ── Plain-date helpers (UTC under the hood so no zone shifts a day) ──const toISO = (d: Date) => d.toISOString().slice(0, 10);const fromISO = (s: string) => new Date(`${s}T00:00:00Z`);const addDays = (s: string, n: number) => {  const d = fromISO(s);  d.setUTCDate(d.getUTCDate() + n);  return toISO(d);};const addMonths = (s: string, n: number) => {  const d = fromISO(s);  const day = d.getUTCDate();  d.setUTCDate(1);  d.setUTCMonth(d.getUTCMonth() + n);  const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();  d.setUTCDate(Math.min(day, last));  return toISO(d);};const monthOf = (s: string) => s.slice(0, 7);const localToday = () => {  const n = new Date();  return `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, "0")}-${String(n.getDate()).padStart(2, "0")}`;};const subscribeDay = (cb: () => void) => {  const t = window.setInterval(cb, 60_000);  return () => window.clearInterval(t);};const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];/** Reads a typed date relative to `today`; null if it can't. */export function parseDate(input: string, today: string): string | null {  const t = input.trim().toLowerCase().replace(/\s+/g, " ");  if (!t) return null;  if (t === "today" || t === "now") return today;  if (t === "tomorrow" || t === "tmr") return addDays(today, 1);  if (t === "yesterday") return addDays(today, -1);  let m = t.match(/^(?:in )?\+?(\d+) ?(d|day|days|w|wk|week|weeks|m|mo|month|months)(?: from now)?$/);  if (m) {    const n = Number(m[1]);    return m[2].startsWith("m") ? addMonths(today, n) : addDays(today, m[2].startsWith("w") ? n * 7 : n);  }  m = t.match(/^(next |this )?(sun|mon|tue|wed|thu|fri|sat)[a-z]*$/);  if (m) {    const want = DAYS.indexOf(m[2]);    const now = fromISO(today).getUTCDay();    let diff = (want - now + 7) % 7;    if (diff === 0 || m[1] === "next ") diff = diff === 0 ? 7 : diff;    return addDays(today, diff);  }  m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);  if (m) return valid(+m[1], +m[2], +m[3]);  // "4 nov", "4 november 2027", "nov 4", "november 4, 2027"  m = t.match(/^(\d{1,2})(?:st|nd|rd|th)? ([a-z]{3})[a-z]*,? ?(\d{4})?$/) ?? null;  if (m) return withYear(+m[1], MONTHS.indexOf(m[2]) + 1, m[3], today);  m = t.match(/^([a-z]{3})[a-z]* (\d{1,2})(?:st|nd|rd|th)?,? ?(\d{4})?$/);  if (m) return withYear(+m[2], MONTHS.indexOf(m[1]) + 1, m[3], today);  // 4/11/2026 reads day first  m = t.match(/^(\d{1,2})[/.](\d{1,2})(?:[/.](\d{2,4}))?$/);  if (m) return withYear(+m[1], +m[2], m[3] ? (m[3].length === 2 ? `20${m[3]}` : m[3]) : undefined, today);  return null;}function valid(y: number, mo: number, d: number) {  if (mo < 1 || mo > 12 || d < 1) return null;  const date = new Date(Date.UTC(y, mo - 1, d));  return date.getUTCMonth() === mo - 1 ? toISO(date) : null;}function withYear(d: number, mo: number, y: string | undefined, today: string) {  if (mo < 1) return null;  if (y) return valid(+y, mo, d);  const thisYear = valid(+today.slice(0, 4), mo, d);  // No year: the next time that date comes round.  return thisYear && thisYear < today ? valid(+today.slice(0, 4) + 1, mo, d) : thisYear;}function Arrow({ back }: { back?: boolean }) {  return (    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="size-3.5">      <path d={back ? "M10 3.5 5.5 8l4.5 4.5" : "M6 3.5 10.5 8 6 12.5"} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />    </svg>  );}export function DatePicker({  value,  defaultValue = null,  onValueChange,  label,  hint,  error,  placeholder = "Pick a date or type one",  min,  max,  weekStartsOn = 1,  locale = "en-GB",  id,  disabled,  className = "",}: DatePickerProps) {  const reduced = useInteractionReducedMotion();  const ids = useFieldIds(id);  const today = React.useSyncExternalStore(subscribeDay, localToday, () => null);  const [own, setOwn] = React.useState<string | null>(defaultValue);  const current = value !== undefined ? value : own;  const [draft, setDraft] = React.useState<string | null>(null);  const [badInput, setBadInput] = React.useState(false);  const [open, setOpen] = React.useState(false);  const [view, setView] = React.useState(() => monthOf(current ?? defaultValue ?? "2026-01-01"));  const [focusDay, setFocusDay] = React.useState<string | null>(null);  const [dir, setDir] = React.useState(0);  const anchor = React.useRef<HTMLDivElement>(null);  const surface = React.useRef<HTMLDivElement>(null);  const grid = React.useRef<HTMLDivElement>(null);  const { mounted, shown } = usePresence(open, 180);  const place = useFloating({ open: mounted, anchor, floating: surface, side: "bottom", align: "start", offset: 6 });  const [vars, setVars] = React.useState<React.CSSProperties>({});  const shakeRef = useShake<HTMLDivElement>(badInput || !!error);  const [mark, setMark] = React.useState<{ x: number; y: number } | null>(null);  const [markReady, setMarkReady] = React.useState(false);  const fmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "short", day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }), [locale]);  const monthFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }), [locale]);  const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }), [locale]);  const headFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "narrow", timeZone: "UTC" }), [locale]);  const allowed = (d: string) => (!min || d >= min) && (!max || d <= max);  const commit = (next: string | null) => {    if (value === undefined) setOwn(next);    onValueChange?.(next);  };  const settle = () => {    if (draft === null) return;    const text = draft.trim();    if (!text) {      setDraft(null);      setBadInput(false);      return commit(null);    }    const parsed = parseDate(text, today ?? localToday());    if (!parsed || !allowed(parsed)) return setBadInput(true);    setDraft(null);    setBadInput(false);    setView(monthOf(parsed));    commit(parsed);  };  const openCalendar = () => {    const start = current ?? today ?? localToday();    setView(monthOf(start));    setFocusDay(start);    setDir(0);    setOpen(true);  };  const close = (refocus: boolean) => {    setOpen(false);    if (refocus) document.getElementById(ids.id)?.focus();  };  useDismiss(open, (reason) => close(reason === "escape"), [anchor, surface]);  React.useLayoutEffect(() => {    if (mounted) setVars(themeVars(anchor.current));  }, [mounted]);  // Days of the shown month, padded to whole weeks.  const days = React.useMemo(() => {    const first = fromISO(`${view}-01`);    const lead = (first.getUTCDay() - weekStartsOn + 7) % 7;    const start = addDays(`${view}-01`, -lead);    const count = Math.ceil((lead + new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate()) / 7) * 7;    return Array.from({ length: count }, (_, i) => addDays(start, i));  }, [view, weekStartsOn]);  const goMonth = (n: number) => {    setDir(n);    setView((v) => monthOf(addMonths(`${v}-01`, n)));    setFocusDay((f) => (f ? addMonths(f, n) : f));  };  // The chosen day's mark glides within a month; a new month slides in from its side.  React.useLayoutEffect(() => {    const g = grid.current;    if (!g) return;    const measure = () => {      const el = current ? g.querySelector<HTMLElement>(`[data-day="${current}"]`) : null;      setMark((p) => (el ? (p && p.x === el.offsetLeft && p.y === el.offsetTop ? p : { x: el.offsetLeft, y: el.offsetTop }) : null));    };    measure();    const ro = new ResizeObserver(measure);    ro.observe(g);    return () => ro.disconnect();  }, [current, view, mounted]);  React.useEffect(() => {    if (!mark || markReady) return;    const f = requestAnimationFrame(() => setMarkReady(true));    return () => cancelAnimationFrame(f);  }, [mark, markReady]);  React.useLayoutEffect(() => {    if (!dir || reduced) return;    grid.current?.animate(      [        { opacity: 0, transform: `translateX(${dir * 24}px)`, filter: "blur(4px)" },        { opacity: 1, transform: "none", filter: "blur(0px)" },      ],      { duration: 360, easing: EASE_OUT },    );  }, [view, dir, reduced]);  // Keep keyboard focus on the focused day once the calendar is up.  React.useEffect(() => {    if (!open || !shown || !focusDay) return;    grid.current?.querySelector<HTMLElement>(`[data-day="${focusDay}"]`)?.focus({ preventScroll: true });  }, [open, shown, focusDay, view]);  const onGridKey = (e: React.KeyboardEvent) => {    if (!focusDay) return;    const moves: Record<string, () => string> = {      ArrowLeft: () => addDays(focusDay, -1),      ArrowRight: () => addDays(focusDay, 1),      ArrowUp: () => addDays(focusDay, -7),      ArrowDown: () => addDays(focusDay, 7),      PageUp: () => addMonths(focusDay, -1),      PageDown: () => addMonths(focusDay, 1),      Home: () => addDays(focusDay, -((fromISO(focusDay).getUTCDay() - weekStartsOn + 7) % 7)),      End: () => addDays(focusDay, 6 - ((fromISO(focusDay).getUTCDay() - weekStartsOn + 7) % 7)),    };    if (e.key in moves) {      e.preventDefault();      const next = moves[e.key]();      if (monthOf(next) !== view) {        setDir(next > focusDay ? 1 : -1);        setView(monthOf(next));      }      setFocusDay(next);    } else if (e.key === "Enter" || e.key === " ") {      e.preventDefault();      if (allowed(focusDay)) {        commit(focusDay);        close(true);      }    }  };  const shownText = draft ?? (current ? fmt.format(fromISO(current)) : "");  const describedBy = [hint && !error && !badInput ? ids.hint : null, error || badInput ? ids.error : null].filter(Boolean).join(" ") || undefined;  return (    <Field id={ids.id} label={label} hint={hint} error={error ?? (badInput ? "Couldn't read that date. Try “tomorrow” or “4 Nov”." : undefined)} className={className}>      <div ref={anchor}>        <div ref={shakeRef} className={`${fieldSurface({ invalid: !!error || badInput, disabled })} h-9 text-[16px] sm:text-[13.5px]`}>          <input            id={ids.id}            value={shownText}            placeholder={placeholder}            disabled={disabled}            aria-invalid={error || badInput ? true : undefined}            aria-describedby={describedBy}            onChange={(e) => {              setDraft(e.target.value);              setBadInput(false);            }}            onBlur={(e) => {              if (!surface.current?.contains(e.relatedTarget as Node)) settle();            }}            onKeyDown={(e) => {              if (e.key === "Enter") {                e.preventDefault();                settle();              } else if (e.key === "ArrowDown" && e.altKey) {                e.preventDefault();                openCalendar();              }            }}            className="h-full min-w-0 flex-1 bg-transparent pl-4 pr-1 outline-none placeholder:text-muted-foreground/70"          />          <button            type="button"            aria-label="Choose date"            aria-haspopup="dialog"            aria-expanded={open}            disabled={disabled}            onClick={() => (open ? close(false) : openCalendar())}            className={`mr-1 flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}          >            <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="size-3.5">              <rect x="2.5" y="3.5" width="11" height="10" rx="2" stroke="currentColor" strokeWidth="1.4" />              <path d="M2.5 6.5h11M5.5 2v3M10.5 2v3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />            </svg>          </button>        </div>      </div>      {mounted &&        createPortal(          <div            ref={surface}            role="dialog"            aria-label="Choose date"            className="fixed left-0 top-0 z-[55] w-[284px] rounded-[18px] bg-popover p-3 text-popover-foreground shadow-[0_0_0_1px_var(--border)]"            style={{ ...vars, left: place.x, top: place.y, transformOrigin: place.origin, ...enterStyle(shown, place.side, reduced) }}          >            <div className="mb-2 flex items-center justify-between pl-2">              <span className="text-[13.5px] font-medium text-foreground" aria-live="polite">                <TextMorph>{monthFmt.format(fromISO(`${view}-01`))}</TextMorph>              </span>              <div className="flex">                {[-1, 1].map((n) => (                  <button                    key={n}                    type="button"                    aria-label={n < 0 ? "Previous month" : "Next month"}                    onClick={() => goMonth(n)}                    className={`flex size-7 items-center justify-center rounded-full text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}                  >                    <Arrow back={n < 0} />                  </button>                ))}              </div>            </div>            <div className="grid grid-cols-7 pb-1 text-center text-[11px] text-muted-foreground" aria-hidden="true">              {days.slice(0, 7).map((d) => (                <span key={d}>{headFmt.format(fromISO(d))}</span>              ))}            </div>            <div ref={grid} role="grid" aria-label={monthFmt.format(fromISO(`${view}-01`))} onKeyDown={onGridKey} className="relative grid grid-cols-7 gap-y-0.5">              <span                aria-hidden="true"                className="pointer-events-none absolute left-0 top-0 size-9 rounded-full bg-primary"                style={{                  transform: `translate3d(${mark?.x ?? 0}px, ${mark?.y ?? 0}px, 0)`,                  opacity: mark ? 1 : 0,                  transition: reduced || !markReady ? "none" : `transform 420ms ${EASE_THROW}, opacity 200ms ${EASE_OUT}`,                }}              />              {days.map((d) => {                const inMonth = monthOf(d) === view;                const on = d === current;                const ok = allowed(d);                return (                  <button                    key={d}                    type="button"                    role="gridcell"                    data-day={d}                    aria-selected={on}                    aria-label={dayFmt.format(fromISO(d))}                    aria-current={d === today ? "date" : undefined}                    tabIndex={d === focusDay ? 0 : -1}                    disabled={!ok}                    onClick={() => {                      commit(d);                      setDraft(null);                      setBadInput(false);                      close(true);                    }}                    className={`relative z-10 mx-auto flex size-9 items-center justify-center rounded-full text-[13px] tabular-nums cursor-pointer transition-colors duration-200 disabled:cursor-default disabled:opacity-30 ${INTERACTION_FOCUS} focus-visible:ring-offset-0 ${                      on ? "text-primary-foreground" : inMonth ? "text-foreground hover:bg-accent" : "text-muted-foreground/50 hover:bg-accent"                    } ${d === today && !on ? "shadow-[inset_0_0_0_1px_var(--border)]" : ""}`}                  >                    {Number(d.slice(8))}                  </button>                );              })}            </div>            <div className="mt-2 flex justify-between border-t border-border pt-2">              <button type="button" onClick={() => { const t = today ?? localToday(); commit(t); setDraft(null); close(true); }} className={`h-7 rounded-full px-2.5 text-[12.5px] font-medium text-foreground cursor-pointer hover:bg-accent ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}>                Today              </button>              {current && (                <button type="button" onClick={() => { commit(null); setDraft(null); close(true); }} className={`h-7 rounded-full px-2.5 text-[12.5px] text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}>                  Clear                </button>              )}            </div>          </div>,          document.body,        )}    </Field>  );}export default DatePicker;
components/field.tsx
"use client";import * as React from "react";import { EASE_OUT, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * FIELD: the column every text control sits in * *   label    above, with an optional aside on the right (a count) *   hint     under the control while all is well *   error    takes the hint's place: the hint folds away, the *            error rises in out of a blur, the control shakes once * * Shared by input, textarea, password, search and select so the * whole family has one surface, one focus ring and one error. * ───────────────────────────────────────────────────────── */export interface FieldProps {  id: string;  label?: React.ReactNode;  /** Right side of the label row, e.g. a character count. */  aside?: React.ReactNode;  hint?: React.ReactNode;  error?: React.ReactNode;  required?: boolean;  className?: string;  children: React.ReactNode;}export type FieldSize = "sm" | "md";/** The surface of a text control: a hairline that darkens on hover and turns to the ring on focus. */export function fieldSurface({ invalid, disabled, round = true }: { invalid?: boolean; disabled?: boolean; round?: boolean }) {  const ring = invalid    ? "shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_55%,transparent)] focus-within:shadow-[inset_0_0_0_1px_var(--color-red-500),0_0_0_3px_color-mix(in_oklab,var(--color-red-500)_14%,transparent)]"    : "shadow-[inset_0_0_0_1px_var(--border)] [&:not(:focus-within)]:hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_24%,var(--border))] focus-within:shadow-[inset_0_0_0_1px_var(--ring),0_0_0_3px_color-mix(in_oklab,var(--ring)_16%,transparent)]";  return `relative flex w-full items-center bg-background text-foreground transition-[box-shadow,background-color,opacity] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] ${round ? "rounded-full" : "rounded-[14px]"} ${ring} ${    disabled ? "pointer-events-none opacity-50" : ""  }`;}/** ids for the control's label, hint and error. */export function useFieldIds(id?: string) {  const auto = React.useId();  const base = id ?? auto;  return { id: base, hint: `${base}-hint`, error: `${base}-error` };}/** One short shake whenever `on` turns true (an error arrives). */export function useShake<T extends HTMLElement>(on: boolean) {  const ref = React.useRef<T>(null);  const reduced = useInteractionReducedMotion();  React.useEffect(() => {    if (!on || reduced) return;    ref.current?.animate(      [{ transform: "none" }, { transform: "translateX(-4px)" }, { transform: "translateX(4px)" }, { transform: "translateX(-2px)" }, { transform: "none" }],      { duration: 380, easing: "ease-out" },    );  }, [on, reduced]);  return ref;}/** Sets a native input's value so React's onChange still fires (clear buttons, pickers). */export function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {  const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;  Object.getOwnPropertyDescriptor(proto, "value")?.set?.call(el, value);  el.dispatchEvent(new Event("input", { bubbles: true }));}/** A line under the control that folds open and closed; keeps its last words while closing. */function Line({ show, children, id, tone, reduced }: { show: boolean; children: React.ReactNode; id: string; tone: string; reduced: boolean }) {  const [kept, setKept] = React.useState(children);  if (show && children !== kept) setKept(children);  return (    <div      className="grid"      style={{ gridTemplateRows: show ? "1fr" : "0fr", transition: reduced ? "none" : `grid-template-rows 320ms ${EASE_OUT}` }}    >      <div className="min-h-0 overflow-hidden">        <p          id={id}          className={`pt-1.5 pl-3 text-[12.5px] leading-snug ${tone}`}          style={{            opacity: show ? 1 : 0,            filter: show ? "blur(0px)" : "blur(4px)",            transform: show ? "none" : "translateY(-4px)",            transition: reduced ? "none" : show ? `opacity 280ms ${EASE_OUT} 60ms, filter 280ms ${EASE_OUT} 60ms, transform 380ms ${EASE_OUT}` : `opacity 140ms ${EASE_OUT}, filter 140ms ${EASE_OUT}, transform 200ms ${EASE_OUT}`,          }}        >          {show ? children : kept}        </p>      </div>    </div>  );}export function Field({ id, label, aside, hint, error, required, className = "", children }: FieldProps) {  const reduced = useInteractionReducedMotion();  const ids = { hint: `${id}-hint`, error: `${id}-error` };  return (    <div className={`flex w-full flex-col ${className}`}>      {(label || aside) && (        <div className="flex items-baseline justify-between gap-3 px-3 pb-1.5">          {label && (            <label htmlFor={id} className="text-[13px] font-medium text-foreground">              {label}              {required && (                <span aria-hidden="true" className="ml-0.5 text-muted-foreground">                  *                </span>              )}            </label>          )}          {aside && <span className="text-[12px] tabular-nums text-muted-foreground">{aside}</span>}        </div>      )}      {children}      <Line show={!error && !!hint} id={ids.hint} tone="text-muted-foreground" reduced={reduced}>        {hint}      </Line>      <Line show={!!error} id={ids.error} tone="text-red-600 dark:text-red-400" reduced={reduced}>        {error}      </Line>    </div>  );}export default Field;
components/floating.ts
"use client";import * as React from "react";/* ───────────────────────────────────────────────────────── * FLOATING: what every popover, menu and tooltip shares * *   place     beside an anchor on a side, lined up start, centre *             or end; flips to the other side when it doesn't fit *             and shifts to stay `padding` inside the viewport; *             follows scroll and resize *   presence  mounts on open, shows a frame later (so the entry *             transitions run), hides, then unmounts after `exit` *   dismiss   Escape and a press outside both close it *   theme     the portal carries the anchor's theme tokens, so a *             light specimen on a dark page stays light * ───────────────────────────────────────────────────────── */export type Side = "top" | "bottom" | "left" | "right";export type Align = "start" | "center" | "end";const TOKENS = ["--background", "--foreground", "--popover", "--popover-foreground", "--border", "--accent", "--accent-foreground", "--primary", "--primary-foreground", "--muted", "--muted-foreground", "--ring", "--card"];/** The anchor's theme tokens as inline styles for something portalled out of its tree. */export function themeVars(anchor: Element | null): React.CSSProperties {  if (!anchor) return {};  const style = getComputedStyle(anchor);  const vars: Record<string, string> = {};  for (const t of TOKENS) {    const v = style.getPropertyValue(t);    if (v) vars[t] = v;  }  return vars as React.CSSProperties;}const OPPOSITE: Record<Side, Side> = { top: "bottom", bottom: "top", left: "right", right: "left" };export interface Placement {  x: number;  y: number;  side: Side;  /** Where the surface grows from, for transform-origin. */  origin: string;  ready: boolean;}/** Positions `floating` (position: fixed) next to `anchor` while `open`. Pass a point instead of an anchor for context menus. */export function useFloating({  open,  anchor,  point,  floating,  side = "bottom",  align = "center",  offset = 8,  padding = 8,}: {  open: boolean;  anchor?: React.RefObject<Element | null>;  point?: { x: number; y: number } | null;  floating: React.RefObject<HTMLElement | null>;  side?: Side;  align?: Align;  offset?: number;  padding?: number;}): Placement {  const [place, setPlace] = React.useState<Placement>({ x: -9999, y: -9999, side, origin: "center", ready: false });  React.useLayoutEffect(() => {    if (!open) return;    let frame = 0;    const measure = () => {      const el = floating.current;      if (!el) return;      const a = point        ? { left: point.x, right: point.x, top: point.y, bottom: point.y, width: 0, height: 0 }        : anchor?.current?.getBoundingClientRect();      if (!a) return;      const w = el.offsetWidth;      const h = el.offsetHeight;      const vw = window.innerWidth;      const vh = window.innerHeight;      const room: Record<Side, number> = { top: a.top - padding, bottom: vh - a.bottom - padding, left: a.left - padding, right: vw - a.right - padding };      const need = side === "top" || side === "bottom" ? h + offset : w + offset;      const s = room[side] < need && room[OPPOSITE[side]] > room[side] ? OPPOSITE[side] : side;      let x: number;      let y: number;      if (s === "top" || s === "bottom") {        y = s === "bottom" ? a.bottom + offset : a.top - offset - h;        x = align === "start" ? a.left : align === "end" ? a.right - w : a.left + a.width / 2 - w / 2;        if (point) x = a.left;      } else {        x = s === "right" ? a.right + offset : a.left - offset - w;        y = align === "start" ? a.top : align === "end" ? a.bottom - h : a.top + a.height / 2 - h / 2;      }      x = Math.max(padding, Math.min(vw - padding - w, x));      y = Math.max(padding, Math.min(vh - padding - h, y));      // Grow from the point on the anchor's edge nearest the surface.      const ox = s === "left" ? "100%" : s === "right" ? "0%" : `${Math.max(0, Math.min(w, a.left + a.width / 2 - x))}px`;      const oy = s === "top" ? "100%" : s === "bottom" ? "0%" : `${Math.max(0, Math.min(h, a.top + a.height / 2 - y))}px`;      const next = { x: Math.round(x), y: Math.round(y), side: s, origin: `${ox} ${oy}`, ready: true };      setPlace((p) => (p.x === next.x && p.y === next.y && p.side === next.side && p.origin === next.origin && p.ready ? p : next));    };    const schedule = () => {      cancelAnimationFrame(frame);      frame = requestAnimationFrame(measure);    };    measure();    const ro = new ResizeObserver(schedule);    if (floating.current) ro.observe(floating.current);    window.addEventListener("scroll", schedule, { capture: true, passive: true });    window.addEventListener("resize", schedule);    return () => {      cancelAnimationFrame(frame);      ro.disconnect();      window.removeEventListener("scroll", schedule, { capture: true });      window.removeEventListener("resize", schedule);    };  }, [open, anchor, point, floating, side, align, offset, padding]);  return open ? place : { ...place, ready: false };}/** Mount → show next frame → hide → unmount after `exit` ms. */export function usePresence(open: boolean, exit = 160) {  const [mounted, setMounted] = React.useState(open);  const [shown, setShown] = React.useState(false);  if (open && !mounted) setMounted(true);  React.useEffect(() => {    if (open) {      let inner = 0;      const outer = requestAnimationFrame(() => {        inner = requestAnimationFrame(() => setShown(true));      });      return () => {        cancelAnimationFrame(outer);        cancelAnimationFrame(inner);      };    }    const timer = window.setTimeout(() => {      setMounted(false);      setShown(false);    }, exit);    return () => window.clearTimeout(timer);  }, [open, exit]);  return { mounted: mounted || open, shown: open && shown };}/** Escape and presses outside `refs` call onDismiss while `open`. */export function useDismiss(open: boolean, onDismiss: (reason: "escape" | "outside") => void, refs: React.RefObject<Element | null>[]) {  const latest = React.useRef({ onDismiss, refs });  React.useEffect(() => {    latest.current = { onDismiss, refs };  });  React.useEffect(() => {    if (!open) return;    const onKey = (e: KeyboardEvent) => {      if (e.key !== "Escape") return;      e.stopPropagation();      latest.current.onDismiss("escape");    };    const onDown = (e: PointerEvent) => {      const target = e.target as Node;      if (latest.current.refs.some((r) => r.current?.contains(target))) return;      latest.current.onDismiss("outside");    };    document.addEventListener("keydown", onKey);    document.addEventListener("pointerdown", onDown, true);    return () => {      document.removeEventListener("keydown", onKey);      document.removeEventListener("pointerdown", onDown, true);    };  }, [open]);}/** The entry pose for a surface on `side`: it comes 6px from the anchor, 0.96 and blurred. */export function enterStyle(shown: boolean, side: Side, reduced: boolean, ease = "cubic-bezier(0.16,1,0.3,1)"): React.CSSProperties {  const d = 6;  const shift = side === "top" ? `0 ${d}px` : side === "bottom" ? `0 -${d}px` : side === "left" ? `${d}px 0` : `-${d}px 0`;  return {    opacity: shown ? 1 : 0,    translate: shown || reduced ? "0 0" : shift,    scale: shown || reduced ? "1" : "0.96",    filter: shown || reduced ? "blur(0px)" : "blur(4px)",    transition: reduced      ? "opacity 120ms ease-out"      : shown        ? `opacity 220ms ${ease}, translate 320ms ${ease}, scale 320ms ${ease}, filter 220ms ${ease}`        : `opacity 140ms ${ease}, translate 180ms ${ease}, scale 180ms ${ease}, filter 140ms ${ease}`,  };}

Registry item: /r/date-picker.json