{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-picker",
  "title": "Date Picker",
  "author": "AlgorizeTech",
  "description": "A single-date field with a popover calendar, typed entry and validation states. Use it for one-off dates such as a deadline or birth date.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/time-picker.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/DatePickerBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  AlertCircle,\n  Calendar as CalendarIcon,\n  ChevronLeft,\n  ChevronRight,\n  Clock,\n} from \"lucide-react\";\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { getThemeStyles } from \"./date-picker-theme\";\nimport { springLinear } from \"./motion-curves\";\nimport { TimePicker } from \"./TimePickerBlock\";\nimport {\n  formatTime,\n  roundToStep,\n  setTime,\n  type TimeFormat,\n} from \"./time-picker-utils\";\n\n// ==========================================\n// DYNAMIC THEME ENGINE\n// ==========================================\nexport type DatePickerColor = \"indigo\" | \"rose\" | \"emerald\" | \"zinc\" | string;\nexport type DatePickerVariant = \"default\" | \"filled\" | \"underlined\" | \"glow\";\nexport type DatePickerSize = \"sm\" | \"md\" | \"lg\";\n\n// ==========================================\n// CALENDAR MATH HELPERS\n// ==========================================\nconst getDaysInMonth = (year: number, month: number) =>\n  new Date(year, month + 1, 0).getDate();\nconst getFirstDayOfMonth = (year: number, month: number) =>\n  new Date(year, month, 1).getDay();\nconst MONTH_NAMES = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n];\nconst DAY_NAMES = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\n\n/**\n * Popover widths: the calendar alone (18rem) and the calendar with the time\n * rail unfolded beside it (30rem). The 12rem difference is exactly the rail\n * plus its gutter, so the calendar grid keeps its size while the rail opens.\n */\nconst POPOVER_PX = 288;\nconst WIDE_POPOVER_PX = 480;\n\n/** The time rail beside the calendar: its w-44 column plus the ml-4 gutter. */\nconst RAIL_PX = 192;\n\n// ==========================================\n// MOTION\n// Same curves as RangeDatePicker so both pickers unfold identically: panels\n// unfold on a spring (stiffness 240, damping 30, mass 0.9, drawn as a CSS\n// `linear()` curve) and fold away on a short ease. Reduced motion: instant.\n// ==========================================\nconst UNFOLD = springLinear({ stiffness: 240, damping: 30, mass: 0.9 });\nconst unfoldOn = (property: string) => `${property} ${UNFOLD.duration}ms ${UNFOLD.easing}`;\nconst settleOn = (property: string) => `${property} 0.28s cubic-bezier(0.22, 1, 0.36, 1)`;\nconst popOn = (property: string) => `${property} 0.2s cubic-bezier(0, 0, 0.58, 1)`;\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n/** The visitor's reduced-motion setting; false while server-rendering. */\nfunction useReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  );\n}\n\ntype FoldPhase = \"closed\" | \"entering\" | \"open\" | \"closing\";\n\n/**\n * A panel that unfolds when `show` turns on and folds away before it leaves:\n * open on the spring, closed on the ease, already open if shown from the\n * start, and still showing what it last showed while it folds. `axis=\"x\"`\n * unfolds its width to `size`; `axis=\"y\"` its height to whatever it holds.\n */\nfunction Fold({\n  show,\n  axis,\n  size,\n  instant,\n  className,\n  children,\n}: {\n  show: boolean;\n  axis: \"x\" | \"y\";\n  size?: number | string;\n  instant: boolean;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const [phase, setPhase] = useState<FoldPhase>(show ? \"open\" : \"closed\");\n  // What it last showed, kept so the panel still holds it while it folds.\n  const [shown, setShown] = useState(children);\n  if (show && shown !== children) setShown(children);\n\n  // Follow `show` while rendering, so the new phase is what first commits.\n  if (show && phase === \"closed\") setPhase(instant ? \"open\" : \"entering\");\n  else if (show && phase === \"closing\") setPhase(\"open\");\n  else if (!show && (phase === \"open\" || phase === \"entering\")) setPhase(instant ? \"closed\" : \"closing\");\n\n  // Entering: the panel mounts with the folded look; commit it, then\n  // transition to the open one before the first paint.\n  const enter = useCallback((el: HTMLDivElement | null) => {\n    if (!el) return;\n    void el.offsetWidth;\n    setPhase((p) => (p === \"entering\" ? \"open\" : p));\n  }, []);\n\n  // In case the fold never reports its end (a hidden tab, `display: none`).\n  useEffect(() => {\n    if (phase !== \"closing\") return;\n    const t = window.setTimeout(() => setPhase(\"closed\"), 400);\n    return () => window.clearTimeout(t);\n  }, [phase]);\n\n  if (phase === \"closed\") return null;\n\n  const open = phase === \"open\";\n  const dimension = axis === \"x\" ? \"width\" : \"grid-template-rows\";\n  const on = phase === \"closing\" ? settleOn : unfoldOn;\n  return (\n    <div\n      ref={enter}\n      className={className}\n      style={{\n        ...(axis === \"x\"\n          ? { width: open ? size : 0 }\n          : { display: \"grid\", gridTemplateRows: open ? \"1fr\" : \"0fr\" }),\n        opacity: open ? 1 : 0,\n        transition: instant ? \"none\" : `${on(dimension)}, ${on(\"opacity\")}`,\n      }}\n      onTransitionEnd={(e) => {\n        if (phase === \"closing\" && e.target === e.currentTarget && e.propertyName === dimension) setPhase(\"closed\");\n      }}\n    >\n      {axis === \"x\" ? shown : <div className=\"min-h-0\">{shown}</div>}\n    </div>\n  );\n}\n\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst endOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);\nconst isSameDay = (d1: Date, d2: Date) =>\n  d1.getDate() === d2.getDate() &&\n  d1.getMonth() === d2.getMonth() &&\n  d1.getFullYear() === d2.getFullYear();\n\n// ==========================================\n// DATEPICKER COMPONENT\n// ==========================================\nexport interface DatePickerBlockProps {\n  value?: Date | null;\n  onChange?: (date: Date) => void;\n  placeholder?: string;\n  variant?: DatePickerVariant;\n  color?: DatePickerColor;\n  inputSize?: DatePickerSize;\n  label?: string;\n  helperText?: string;\n  error?: string;\n  disabled?: boolean;\n  /** Greys out every day before today so it cannot be picked. */\n  disablePast?: boolean;\n  /** Earliest selectable day (inclusive). Combine with `disablePast` freely — the later bound wins. */\n  minDate?: Date;\n  /** Latest selectable day (inclusive). */\n  maxDate?: Date;\n  /**\n   * Adds hour / minute (and AM-PM) columns. The popover opens on the calendar\n   * alone and unfolds the columns — beside it when there is room, below it\n   * otherwise — as soon as a day is picked, staying open until Done. `value`\n   * then carries the time as well as the day.\n   */\n  showTime?: boolean;\n  /** 12-hour clock with AM / PM, or 24-hour. Default \"12h\". */\n  timeFormat?: TimeFormat;\n  /** Minute granularity of the time columns. Default 5. */\n  minuteStep?: number;\n  className?: string;\n}\n\nexport const DatePickerBlock = forwardRef<HTMLDivElement, DatePickerBlockProps>(\n  (\n    {\n      value,\n      onChange,\n      placeholder = \"Pick a date\",\n      variant = \"default\",\n      color = \"primary\",\n      inputSize = \"md\",\n      label,\n      helperText,\n      error,\n      disabled = false,\n      disablePast = false,\n      minDate,\n      maxDate,\n      showTime = false,\n      timeFormat = \"12h\",\n      minuteStep = 5,\n      className,\n    },\n    ref,\n  ) => {\n    const [isOpen, setIsOpen] = useState(false);\n    const [viewDate, setViewDate] = useState(value || new Date());\n    // The time a freshly picked day receives — \"now\" rounded up to the step.\n    // The columns only exist once a day is chosen, so this seeds that first\n    // pick rather than being something the user edits beforehand.\n    const [pendingTime, setPendingTime] = useState(() => roundToStep(new Date(), minuteStep));\n    // With time columns the popover can go wide and put them to the right of\n    // the calendar — but only when the viewport has room for that width, so it\n    // never runs off-screen in a narrow column or on a phone. `alignRight`\n    // flips the popover to hang from the trigger's right edge when that is the\n    // side with space.\n    const [sideLayout, setSideLayout] = useState(false);\n    const [alignRight, setAlignRight] = useState(false);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const { theme, style } = getThemeStyles(color);\n\n    // containerRef drives the click-outside handler, so the forwarded ref is\n    // mirrored onto it rather than replacing it.\n    React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n    const hasError = Boolean(error);\n\n    const reduceMotion = useReducedMotion();\n    // The time panel is absent until a day lands: the popover opens on the\n    // calendar alone, unfolds on the first pick, and folds away again if the\n    // date is cleared. Mirrors `railOpen` in RangeDatePicker.\n    const railOpen = showTime && Boolean(value);\n\n    // Close on click outside\n    useEffect(() => {\n      // Resolve the document the picker actually lives in (supports iframe rendering).\n      const doc = containerRef.current?.ownerDocument ?? document;\n      const handleClickOutside = (event: MouseEvent) => {\n        if (\n          containerRef.current &&\n          !containerRef.current.contains(event.target as Node)\n        ) {\n          setIsOpen(false);\n        }\n      };\n      doc.addEventListener(\"mousedown\", handleClickOutside);\n      return () => doc.removeEventListener(\"mousedown\", handleClickOutside);\n    }, []);\n\n    // Sync view date with value when opened (or when value changes while open).\n    // Tracked while rendering rather than in an effect, so the calendar never\n    // paints a frame on the old month first.\n    const [syncedTo, setSyncedTo] = useState({ isOpen, value });\n    if (syncedTo.isOpen !== isOpen || syncedTo.value !== value) {\n      setSyncedTo({ isOpen, value });\n      if (isOpen && value) setViewDate(value);\n    }\n\n    // Measure the room on either side of the trigger whenever the popover opens\n    // or the window resizes, and pick the widest layout that fits on-screen.\n    useEffect(() => {\n      if (!isOpen) return;\n      const el = containerRef.current;\n      if (!el) return;\n      const win = el.ownerDocument.defaultView ?? window;\n      const MARGIN = 16;\n      const measure = () => {\n        const rect = el.getBoundingClientRect();\n        const roomRight = win.innerWidth - rect.left - MARGIN;\n        const roomLeft = rect.right - MARGIN;\n        const wide = showTime && (roomRight >= WIDE_POPOVER_PX || roomLeft >= WIDE_POPOVER_PX);\n        const need = wide ? WIDE_POPOVER_PX : POPOVER_PX;\n        setSideLayout(wide);\n        // Hang from the right edge only when the left-anchored popover would\n        // overflow the viewport and the other side actually has the room.\n        setAlignRight(roomRight < need && roomLeft >= need);\n      };\n      measure();\n      win.addEventListener(\"resize\", measure);\n      return () => win.removeEventListener(\"resize\", measure);\n    }, [showTime, isOpen]);\n\n    // --- Calendar Generation ---\n    const currentMonth = viewDate.getMonth();\n    const currentYear = viewDate.getFullYear();\n    const daysInMonth = getDaysInMonth(currentYear, currentMonth);\n    const firstDayIndex = getFirstDayOfMonth(currentYear, currentMonth);\n\n    const handlePrevMonth = (e: React.MouseEvent) => {\n      e.stopPropagation();\n      setViewDate(new Date(currentYear, currentMonth - 1, 1));\n    };\n\n    const handleNextMonth = (e: React.MouseEvent) => {\n      e.stopPropagation();\n      setViewDate(new Date(currentYear, currentMonth + 1, 1));\n    };\n\n    // --- Bounds ---\n    // `disablePast` is sugar for minDate = today; whichever lower bound is\n    // later wins, so both can be passed together without surprises.\n    const today = new Date();\n    const lowerCandidates = [\n      disablePast ? startOfDay(today) : null,\n      minDate ? startOfDay(minDate) : null,\n    ].filter(Boolean) as Date[];\n    const lower = lowerCandidates.length\n      ? new Date(Math.max(...lowerCandidates.map((d) => d.getTime())))\n      : null;\n    const upper = maxDate ? endOfDay(maxDate) : null;\n    const isDayDisabled = (date: Date) =>\n      (lower !== null && endOfDay(date) < lower) ||\n      (upper !== null && startOfDay(date) > upper);\n\n    // Time bounds only bite on the boundary days themselves — any other day\n    // has every time available. On today, with `disablePast`, the floor is the\n    // current moment rather than midnight.\n    const timeSource = value ?? pendingTime;\n    const minTime =\n      lower && value && isSameDay(value, lower)\n        ? disablePast && isSameDay(lower, today)\n          ? today\n          : lower\n        : null;\n    const maxTime = upper && value && isSameDay(value, upper) ? upper : null;\n\n    const handleSelectDate = (day: number, e: React.MouseEvent) => {\n      e.stopPropagation();\n      let newDate = new Date(currentYear, currentMonth, day);\n      if (isDayDisabled(newDate)) return;\n      if (showTime) {\n        newDate = setTime(newDate, timeSource.getHours(), timeSource.getMinutes());\n        // Landing on today with a time that has already passed: bump to now.\n        if (disablePast && isSameDay(newDate, today) && newDate < today) {\n          newDate = roundToStep(today, minuteStep);\n        }\n      }\n      if (onChange) onChange(newDate);\n      // With time columns the popover stays open so the time can be set too.\n      if (!showTime) setIsOpen(false);\n    };\n\n    const handleTimeChange = (next: Date) => {\n      if (value) onChange?.(next);\n      else setPendingTime(next);\n    };\n\n    // --- Formatting ---\n    const formatDate = (date: Date) => {\n      const day = new Intl.DateTimeFormat(\"en-US\", {\n        month: \"short\",\n        day: \"numeric\",\n        year: \"numeric\",\n      }).format(date);\n      return showTime ? `${day}, ${formatTime(date, timeFormat)}` : day;\n    };\n\n    // --- Size Classes ---\n    const sizeClasses = {\n      sm: {\n        wrapper: \"min-h-9 px-3 py-1.5\",\n        text: \"text-xs\",\n        icon: \"w-3.5 h-3.5\",\n      },\n      md: { wrapper: \"min-h-11 px-4 py-2.5\", text: \"text-sm\", icon: \"w-4 h-4\" },\n      lg: {\n        wrapper: \"min-h-14 px-5 py-3.5\",\n        text: \"text-base\",\n        icon: \"w-5 h-5\",\n      },\n    };\n    const s = sizeClasses[inputSize];\n\n    // --- Variant Styles ---\n    const getVariantStyles = () => {\n      const baseWrapper = cn(\n        \"relative flex items-center justify-between w-full transition-all duration-300 cursor-pointer outline-none\",\n        s.wrapper,\n      );\n\n      if (disabled)\n        return {\n          wrapper: cn(\n            baseWrapper,\n            \"opacity-50 cursor-not-allowed border border-border bg-muted\",\n          ),\n          active: \"\",\n        };\n      if (hasError)\n        return {\n          wrapper: cn(\n            baseWrapper,\n            \"border border-red-500 bg-background\",\n            variant === \"underlined\" && \"border-0 border-b-2 rounded-none\",\n            variant !== \"underlined\" && \"rounded-xl\",\n          ),\n          active: \"ring-4 ring-red-500/10\",\n        };\n\n      switch (variant) {\n        case \"filled\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-muted border border-transparent rounded-xl\",\n            ),\n            active: cn(\"bg-background ring-4\", theme.ring),\n          };\n        case \"underlined\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"border-b-2 border-border bg-transparent rounded-none px-0\",\n            ),\n            active: theme.ring.split(\" \")[1],\n          };\n        case \"glow\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-background border border-border rounded-xl shadow-sm\",\n            ),\n            active: theme.glow,\n          };\n        case \"default\":\n        default:\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-background border border-border rounded-xl shadow-sm\",\n            ),\n            active: cn(\"ring-4\", theme.ring),\n          };\n      }\n    };\n\n    const styles = getVariantStyles();\n\n    // Only the side rail widens the popover; the stacked panel grows downward.\n    const popoverWidth = railOpen && sideLayout ? WIDE_POPOVER_PX : POPOVER_PX;\n\n    const timeHeader = (\n      <div className=\"flex items-center justify-between mb-2\">\n        <span className=\"flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">\n          <Clock className=\"w-3.5 h-3.5\" /> Time\n        </span>\n        <span\n          className={cn(\n            \"text-xs font-semibold tabular-nums\",\n            value ? theme.text : \"text-muted-foreground\",\n          )}\n        >\n          {formatTime(timeSource, timeFormat)}\n        </span>\n      </div>\n    );\n\n    return (\n      <div\n        ref={containerRef}\n        className={cn(\"flex flex-col gap-1.5 w-full relative\", className)}\n        style={style}\n      >\n        {/* Label */}\n        {label && (\n          <label\n            className={cn(\n              \"text-sm font-semibold\",\n              hasError ? \"text-red-500\" : \"text-foreground\",\n              disabled && \"opacity-50\",\n            )}\n          >\n            {label}\n          </label>\n        )}\n\n        {/* Trigger Button */}\n        <button\n          type=\"button\"\n          disabled={disabled}\n          onClick={() => setIsOpen(!isOpen)}\n          className={cn(styles.wrapper, isOpen && styles.active)}\n        >\n          <div className=\"flex items-center gap-2 overflow-hidden w-full\">\n            <div\n              className={cn(\n                \"shrink-0 text-muted-foreground\",\n                isOpen && theme.text,\n              )}\n            >\n              <CalendarIcon className={s.icon} />\n            </div>\n            <span\n              className={cn(\n                \"truncate\",\n                s.text,\n                !value\n                  ? \"text-muted-foreground\"\n                  : \"text-foreground\",\n              )}\n            >\n              {value ? formatDate(value) : placeholder}\n            </span>\n          </div>\n\n          <div className=\"absolute right-4 flex items-center gap-2 shrink-0 bg-transparent\">\n            {hasError && <AlertCircle className={cn(s.icon, \"text-red-500\")} />}\n          </div>\n        </button>\n\n        {/* Calendar Popover — grows in step with the time rail beside it.\n            Closed, it is out of the tab order as well as out of sight. */}\n        <div\n          inert={!isOpen}\n          style={{\n            width: popoverWidth,\n            opacity: isOpen ? 1 : 0,\n            scale: `1 ${isOpen ? 1 : 0.95}`,\n            translate: `0 ${isOpen ? 0 : -8}px`,\n            transition: reduceMotion\n              ? \"none\"\n              : [unfoldOn(\"width\"), popOn(\"opacity\"), popOn(\"scale\"), popOn(\"translate\")].join(\", \"),\n          }}\n          className={cn(\n            \"absolute z-50 overflow-hidden bg-popover text-popover-foreground border border-border rounded-xl shadow-xl origin-top\",\n            alignRight ? \"right-0\" : \"left-0\",\n            // Never wider than the viewport minus a margin, so a phone still\n            // sees the whole thing.\n            \"max-w-[calc(100vw-2rem)]\",\n            variant === \"underlined\" ? \"top-[100%]\" : \"top-[calc(100%+8px)]\",\n            isOpen ? \"pointer-events-auto\" : \"pointer-events-none\",\n          )}\n        >\n          <div className=\"p-4\">\n            <div className={cn(showTime && sideLayout && \"flex items-stretch\")}>\n              <div className=\"min-w-0 flex-1\">\n                {/* Header */}\n                <div className=\"flex items-center justify-between mb-4\">\n                  <button\n                    onClick={handlePrevMonth}\n                    className=\"p-1.5 rounded-md text-muted-foreground hover:bg-muted transition-colors\"\n                  >\n                    <ChevronLeft className=\"w-4 h-4\" />\n                  </button>\n                  <div className=\"text-sm font-semibold text-foreground\">\n                    {MONTH_NAMES[currentMonth]} {currentYear}\n                  </div>\n                  <button\n                    onClick={handleNextMonth}\n                    className=\"p-1.5 rounded-md text-muted-foreground hover:bg-muted transition-colors\"\n                  >\n                    <ChevronRight className=\"w-4 h-4\" />\n                  </button>\n                </div>\n\n                {/* Days Header */}\n                <div className=\"grid grid-cols-7 mb-2\">\n                  {DAY_NAMES.map((day) => (\n                    <div\n                      key={day}\n                      className=\"text-center text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\"\n                    >\n                      {day}\n                    </div>\n                  ))}\n                </div>\n\n                {/* Days Grid */}\n                <div className=\"grid grid-cols-7 gap-1\">\n                  {/* Empty slots for start of month */}\n                  {Array.from({ length: firstDayIndex }).map((_, i) => (\n                    <div key={`empty-${i}`} className=\"w-8 h-8\" />\n                  ))}\n\n                  {/* Actual Days */}\n                  {Array.from({ length: daysInMonth }).map((_, i) => {\n                    const day = i + 1;\n                    const thisDate = new Date(currentYear, currentMonth, day);\n                    const isSelected = value && isSameDay(thisDate, value);\n                    const isToday = isSameDay(thisDate, today);\n                    const isOff = isDayDisabled(thisDate);\n\n                    return (\n                      <button\n                        key={day}\n                        type=\"button\"\n                        disabled={isOff}\n                        aria-disabled={isOff}\n                        onClick={(e) => handleSelectDate(day, e)}\n                        className={cn(\n                          \"w-8 h-8 flex items-center justify-center rounded-full text-sm transition-all duration-200 cursor-pointer mx-auto\",\n                          isSelected\n                            ? cn(theme.bg, \"font-semibold shadow-sm scale-110\")\n                            : isToday\n                              ? cn(\n                                  \"font-semibold bg-muted\",\n                                  theme.text,\n                                )\n                              : \"text-foreground font-medium hover:bg-muted\",\n                          isOff &&\n                            \"text-muted-foreground/40 line-through cursor-not-allowed hover:bg-transparent font-normal\",\n                        )}\n                      >\n                        {day}\n                      </button>\n                    );\n                  })}\n                </div>\n              </div>\n\n              {/* Time — unfolds to the RIGHT of the calendar on the first pick\n                  when there is room, and DOWN from under it otherwise. The\n                  inner column is a fixed width so the lists are never squeezed\n                  mid-unfold, and its margin rides along so the calendar holds\n                  still. */}\n              <Fold\n                show={railOpen && sideLayout}\n                axis=\"x\"\n                size={RAIL_PX}\n                instant={reduceMotion}\n                className=\"shrink-0 overflow-hidden\"\n              >\n                <div className=\"flex h-full w-44 ml-4 flex-col border-l border-border pl-4\">\n                  {timeHeader}\n                  {/* The picker fills whatever height the calendar gives\n                      the row — absolutely positioned so its long lists\n                      never stretch the popover themselves. */}\n                  <div className=\"relative flex-1 min-h-32\">\n                    <TimePicker\n                      value={timeSource}\n                      onChange={handleTimeChange}\n                      format={timeFormat}\n                      minuteStep={minuteStep}\n                      minTime={minTime}\n                      maxTime={maxTime}\n                      color={color}\n                      className=\"absolute inset-0 h-auto\"\n                    />\n                  </div>\n                </div>\n              </Fold>\n            </div>\n\n            <Fold show={railOpen && !sideLayout} axis=\"y\" instant={reduceMotion} className=\"overflow-hidden\">\n              <div className=\"mt-4 pt-4 border-t border-border\">\n                {timeHeader}\n                <TimePicker\n                  value={timeSource}\n                  onChange={handleTimeChange}\n                  format={timeFormat}\n                  minuteStep={minuteStep}\n                  minTime={minTime}\n                  maxTime={maxTime}\n                  color={color}\n                />\n              </div>\n            </Fold>\n\n            {/* Done confirms the day and its time, so it rides in with the\n                panel rather than sitting under an empty calendar. */}\n            <Fold show={railOpen} axis=\"y\" instant={reduceMotion} className=\"overflow-hidden\">\n              <button\n                type=\"button\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  setIsOpen(false);\n                }}\n                className={cn(\n                  \"mt-3 w-full h-9 rounded-lg text-sm font-semibold transition-colors\",\n                  theme.bg,\n                )}\n              >\n                Done\n              </button>\n            </Fold>\n          </div>\n        </div>\n\n        {/* Helper & Error Text */}\n        {error && (\n          <p className=\"text-xs font-semibold text-red-500 flex items-center gap-1 mt-0.5\">\n            <AlertCircle className=\"w-3.5 h-3.5\" /> {error}\n          </p>\n        )}\n        {helperText && !error && (\n          <p className=\"text-xs font-medium text-muted-foreground mt-0.5\">\n            {helperText}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nDatePickerBlock.displayName = \"DatePickerBlock\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/DatePickerBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/date-picker-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    ring: \"focus-within:ring-primary/30 focus-within:border-primary/30\",\n    text: \"text-primary\",\n    bg: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n    bgSoft:\n      \"bg-primary/10 text-foreground hover:bg-primary/20\",\n    glow: \"focus-within:shadow-[0_0_20px_var(--primary)] focus-within:border-primary/30\",\n  },\n  indigo: {\n    ring: \"focus-within:ring-indigo-500/30 focus-within:border-indigo-500\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    bg: \"bg-indigo-600 text-white hover:bg-indigo-700\",\n    bgSoft:\n      \"bg-indigo-50 dark:bg-indigo-500/10 text-indigo-900 dark:text-indigo-100 hover:bg-indigo-100 dark:hover:bg-indigo-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(99,102,241,0.4)] focus-within:border-indigo-500\",\n  },\n  rose: {\n    ring: \"focus-within:ring-rose-500/30 focus-within:border-rose-500\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    bg: \"bg-rose-600 text-white hover:bg-rose-700\",\n    bgSoft:\n      \"bg-rose-50 dark:bg-rose-500/10 text-rose-900 dark:text-rose-100 hover:bg-rose-100 dark:hover:bg-rose-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(244,63,94,0.4)] focus-within:border-rose-500\",\n  },\n  emerald: {\n    ring: \"focus-within:ring-emerald-500/30 focus-within:border-emerald-500\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    bg: \"bg-emerald-600 text-white hover:bg-emerald-700\",\n    bgSoft:\n      \"bg-emerald-50 dark:bg-emerald-500/10 text-emerald-900 dark:text-emerald-100 hover:bg-emerald-100 dark:hover:bg-emerald-500/20\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(16,185,129,0.4)] focus-within:border-emerald-500\",\n  },\n  zinc: {\n    ring: \"focus-within:ring-zinc-500/30 focus-within:border-zinc-500 dark:focus-within:ring-white/20 dark:focus-within:border-white\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    bg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200\",\n    bgSoft:\n      \"bg-zinc-100 dark:bg-zinc-800/50 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-800\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(255,255,255,0.15)] focus-within:border-zinc-900 dark:focus-within:border-white\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string) => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        ring: \"focus-within:ring-[var(--theme-color)]/30 focus-within:border-[var(--theme-color)]\",\n        text: \"text-[var(--theme-color)]\",\n        bg: \"bg-[var(--theme-color)] text-white hover:opacity-90\",\n        bgSoft:\n          \"bg-[rgba(var(--theme-color-rgb),0.1)] text-[var(--theme-color)] hover:bg-[rgba(var(--theme-color-rgb),0.2)]\",\n        glow: \"focus-within:shadow-[0_0_20px_rgba(var(--theme-color-rgb),0.5)] focus-within:border-[var(--theme-color)]\",\n      },\n      style: {\n        \"--theme-color\": color,\n        \"--theme-color-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/date-picker-theme.ts"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    },
    {
      "path": "src/components/custom/blocks/time-picker-utils.ts",
      "content": "export type TimeFormat = \"12h\" | \"24h\";\n\n/** A new Date on the same day as `base` at the given time. */\nexport const setTime = (base: Date, hours: number, minutes: number) => {\n  const next = new Date(base);\n  next.setHours(hours, minutes, 0, 0);\n  return next;\n};\n\n/** Rounds `date` up to the next multiple of `step` minutes — \"now\" for a picker default. */\nexport const roundToStep = (date: Date, step: number) => {\n  const next = new Date(date);\n  const m = Math.ceil(next.getMinutes() / step) * step;\n  next.setMinutes(m, 0, 0);\n  return next;\n};\n\nexport const formatTime = (date: Date, format: TimeFormat = \"12h\") =>\n  new Intl.DateTimeFormat(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    hour12: format === \"12h\",\n  }).format(date);\n",
      "type": "registry:component",
      "target": "components/custom/blocks/time-picker-utils.ts"
    }
  ],
  "docs": "Named exports: DatePickerBlock, getThemeStyles (from date-picker-theme.ts, which installs alongside). Controlled via `value: Date | null` and `onChange`. Shares the variant, color and size vocabulary of the `input` item, so the two line up visually in the same form. For month grids or event display, use `calendar` instead.",
  "categories": [
    "forms",
    "dates"
  ],
  "type": "registry:component"
}
