{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"date-picker","type":"registry:ui","title":"Date Picker","description":"A date field that opens a calendar. Months slide in the direction you page, and arrow keys move between days.","dependencies":["@radix-ui/react-popover@^1.1.23","@radix-ui/react-slot@^1.3.3","@untitledui/icons@^0.0.23","class-variance-authority@^0.7.1","dayjs@^1.11.23","motion@^14.0.0"],"files":[{"path":"components/ui/calendar.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from \"@untitledui/icons\";\nimport dayjs, { type Dayjs } from \"dayjs\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { fieldSurface } from \"@/lib/field\";\nimport { chewyEase } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\nconst dateFormat = \"YYYY-MM-DD\";\nconst weekdays = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\n\ntype CalendarProps = {\n  /** Selected date as `YYYY-MM-DD`. */\n  value?: string;\n  onChange: (date: string) => void;\n  min?: string;\n  max?: string;\n  className?: string;\n};\n\nfunction Calendar({ value, onChange, min, max, className }: CalendarProps) {\n  const selected = value ? dayjs(value) : undefined;\n  const initial = () => {\n    if (selected) return selected;\n    if (min && dayjs().isBefore(min, \"day\")) return dayjs(min);\n    if (max && dayjs().isAfter(max, \"day\")) return dayjs(max);\n    return dayjs();\n  };\n  const [focused, setFocused] = React.useState<Dayjs>(initial);\n  const [month, setMonth] = React.useState<Dayjs>(() => initial().startOf(\"month\"));\n  const [direction, setDirection] = React.useState(0);\n  const container = React.useRef<HTMLDivElement>(null);\n  const moveFocus = React.useRef(false);\n\n  const disabled = (date: Dayjs) =>\n    (min !== undefined && date.isBefore(min, \"day\")) || (max !== undefined && date.isAfter(max, \"day\"));\n\n  const showMonth = (next: Dayjs) => {\n    setDirection(next.isAfter(month) ? 1 : -1);\n    setMonth(next.startOf(\"month\"));\n  };\n\n  const focusDate = (next: Dayjs) => {\n    setFocused(next);\n    moveFocus.current = true;\n    if (!next.isSame(month, \"month\")) showMonth(next);\n  };\n\n  React.useEffect(() => {\n    if (!moveFocus.current) return;\n    moveFocus.current = false;\n    container.current\n      ?.querySelector<HTMLButtonElement>(\n        `[data-month=\"${focused.format(\"YYYY-MM\")}\"] [data-date=\"${focused.format(dateFormat)}\"]`,\n      )\n      ?.focus();\n  });\n\n  const start = month.startOf(\"week\");\n  const days = Array.from({ length: 42 }, (_, i) => start.add(i, \"day\"));\n  const inMonth = days.filter((date) => date.isSame(month, \"month\") && !disabled(date));\n  const tabbable = focused.isSame(month, \"month\") && !disabled(focused) ? focused : inMonth[0];\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const steps: Record<string, Dayjs> = {\n      ArrowLeft: focused.subtract(1, \"day\"),\n      ArrowRight: focused.add(1, \"day\"),\n      ArrowUp: focused.subtract(1, \"week\"),\n      ArrowDown: focused.add(1, \"week\"),\n      PageUp: focused.subtract(1, \"month\"),\n      PageDown: focused.add(1, \"month\"),\n      Home: focused.startOf(\"week\"),\n      End: focused.endOf(\"week\"),\n    };\n    const next = steps[event.key];\n    if (!next) return;\n    event.preventDefault();\n    focusDate(next);\n  };\n\n  return (\n    <div data-slot=\"calendar\" className={cn(\"flex w-[17rem] flex-col gap-3\", className)}>\n      <div className=\"flex items-center justify-between\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"size-8\"\n          aria-label=\"Previous month\"\n          onClick={() => showMonth(month.subtract(1, \"month\"))}\n        >\n          <ChevronLeft />\n        </Button>\n        <span aria-live=\"polite\" className=\"text-sm font-medium\">\n          {month.format(\"MMMM YYYY\")}\n        </span>\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"size-8\"\n          aria-label=\"Next month\"\n          onClick={() => showMonth(month.add(1, \"month\"))}\n        >\n          <ChevronRight />\n        </Button>\n      </div>\n      <div className=\"grid grid-cols-7 text-center\">\n        {weekdays.map((day) => (\n          <span key={day} className=\"eyebrow text-muted-foreground text-[10px]\">\n            {day}\n          </span>\n        ))}\n      </div>\n      <div ref={container} className=\"relative overflow-hidden\">\n        <AnimatePresence initial={false} mode=\"popLayout\" custom={direction}>\n          <motion.div\n            key={month.format(\"YYYY-MM\")}\n            role=\"grid\"\n            data-month={month.format(\"YYYY-MM\")}\n            aria-label={month.format(\"MMMM YYYY\")}\n            custom={direction}\n            variants={{\n              enter: (dir: number) => ({ x: dir * 40, opacity: 0 }),\n              center: { x: 0, opacity: 1 },\n              exit: (dir: number) => ({ x: dir * -40, opacity: 0 }),\n            }}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n            transition={{ duration: 0.25, ease: chewyEase }}\n            onKeyDown={onKeyDown}\n            className=\"grid grid-cols-7 gap-0.5\"\n          >\n            {days.map((date) => {\n              const key = date.format(dateFormat);\n              const isSelected = selected?.isSame(date, \"day\") ?? false;\n              const isToday = date.isSame(dayjs(), \"day\");\n              const outside = !date.isSame(month, \"month\");\n              const isDisabled = disabled(date);\n              return (\n                <button\n                  key={key}\n                  type=\"button\"\n                  data-date={key}\n                  tabIndex={tabbable?.isSame(date, \"day\") ? 0 : -1}\n                  disabled={isDisabled}\n                  aria-pressed={isSelected}\n                  aria-label={date.format(\"dddd, MMMM D, YYYY\")}\n                  onClick={() => {\n                    setFocused(date);\n                    onChange(key);\n                  }}\n                  className={cn(\n                    \"relative flex aspect-square items-center justify-center rounded-md text-sm tabular-nums transition-colors outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-foreground\",\n                    \"disabled:text-muted-foreground/40 disabled:cursor-not-allowed\",\n                    isSelected\n                      ? \"bg-foreground text-background\"\n                      : \"hover:bg-accent enabled:hover:text-accent-foreground\",\n                    outside && !isSelected && \"text-muted-foreground/60\",\n                  )}\n                >\n                  {date.date()}\n                  {isToday && (\n                    <span\n                      aria-hidden\n                      className={cn(\n                        \"absolute bottom-1 size-1 rounded-full\",\n                        isSelected ? \"bg-background\" : \"bg-foreground\",\n                      )}\n                    />\n                  )}\n                </button>\n              );\n            })}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n\ntype DatePickerProps = Omit<CalendarProps, \"className\"> & {\n  id?: string;\n  placeholder?: string;\n  format?: string;\n  onBlur?: () => void;\n  className?: string;\n  \"aria-invalid\"?: boolean;\n};\n\nfunction DatePicker({\n  value,\n  onChange,\n  min,\n  max,\n  id,\n  placeholder = \"Pick a date\",\n  format = \"ddd, MMM D, YYYY\",\n  onBlur,\n  className,\n  \"aria-invalid\": invalid,\n}: DatePickerProps) {\n  const [open, setOpen] = React.useState(false);\n  const calendarId = React.useId();\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next);\n        if (!next) onBlur?.();\n      }}\n    >\n      <PopoverTrigger asChild>\n        <button\n          id={id}\n          type=\"button\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-controls={calendarId}\n          data-slot=\"date-picker\"\n          aria-invalid={invalid}\n          data-state={open ? \"open\" : \"closed\"}\n          className={cn(\n            fieldSurface,\n            \"flex h-9 w-full items-center gap-2 px-3 text-left text-sm tabular-nums data-[state=open]:field-halo focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-foreground aria-invalid:focus-visible:outline-destructive\",\n            !value && \"text-muted-foreground\",\n            className,\n          )}\n        >\n          <CalendarIcon className=\"text-muted-foreground size-4 shrink-0\" />\n          <span className=\"truncate\">{value ? dayjs(value).format(format) : placeholder}</span>\n        </button>\n      </PopoverTrigger>\n      <PopoverContent\n        id={calendarId}\n        align=\"start\"\n        className=\"w-auto p-3\"\n        onOpenAutoFocus={(event) => {\n          event.preventDefault();\n          requestAnimationFrame(() =>\n            document.getElementById(calendarId)?.querySelector<HTMLButtonElement>('[data-date][tabindex=\"0\"]')?.focus(),\n          );\n        }}\n      >\n        <Calendar\n          value={value}\n          min={min}\n          max={max}\n          onChange={(date) => {\n            onChange(date);\n            setOpen(false);\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport { Calendar, DatePicker };\n"},{"path":"components/ui/button.tsx","type":"registry:ui","content":"import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { type HTMLMotionProps, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-foreground aria-invalid:focus-visible:outline-destructive aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground bg-[linear-gradient(to_bottom,rgb(255_255_255/0.12),transparent)] shadow-[inset_0_1px_0_rgb(255_255_255/0.18),0_1px_2px_rgb(0_0_0/0.2),0_0_0_1px_var(--primary)] hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white bg-[linear-gradient(to_bottom,rgb(255_255_255/0.14),transparent)] shadow-[inset_0_1px_0_rgb(255_255_255/0.2),0_1px_2px_rgb(0_0_0/0.2),0_0_0_1px_var(--destructive)] hover:bg-destructive/90\",\n        outline:\n          \"border bg-background shadow-[0_1px_2px_rgb(0_0_0/0.05),inset_0_-1px_0_rgb(0_0_0/0.05)] hover:bg-accent/60 hover:border-foreground/20 dark:bg-input/30 dark:border-input dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.05)] dark:hover:bg-input/50\",\n        secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/70\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-foreground underline underline-offset-4 decoration-foreground/30 hover:decoration-foreground\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const classes = cn(buttonVariants({ variant, size, className }));\n\n  if (asChild) {\n    return <Slot data-slot=\"button\" className={classes} {...props} />;\n  }\n\n  return (\n    <motion.button\n      data-slot=\"button\"\n      whileTap={{ scale: 0.97 }}\n      transition={{ type: \"spring\", stiffness: 600, damping: 30 }}\n      className={classes}\n      {...(props as HTMLMotionProps<\"button\">)}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"},{"path":"components/ui/popover.tsx","type":"registry:ui","content":"\"use client\";\n\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport { createAnimatedRoot, popMotion } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\nconst [Popover, usePopoverOpenState] = createAnimatedRoot(PopoverPrimitive.Root, \"popover\");\n\nfunction PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  children,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  const { open, onExitComplete } = usePopoverOpenState();\n\n  return (\n    <AnimatePresence onExitComplete={onExitComplete}>\n      {open && (\n        <PopoverPrimitive.Portal forceMount>\n          <PopoverPrimitive.Content\n            data-slot=\"popover-content\"\n            align={align}\n            sideOffset={sideOffset}\n            forceMount\n            className=\"z-50 outline-hidden\"\n            {...props}\n          >\n            <motion.div\n              {...popMotion}\n              className={cn(\n                \"frosted text-popover-foreground w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-float\",\n                className,\n              )}\n            >\n              {children}\n            </motion.div>\n          </PopoverPrimitive.Content>\n        </PopoverPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nexport { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };\n"},{"path":"lib/field.ts","type":"registry:lib","content":"export const fieldSurface =\n  \"bg-background border-input dark:bg-input/30 rounded-lg border shadow-xs transition-[border-color,box-shadow] outline-none hover:border-foreground/25 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive\";\n\nexport function isFieldInvalid(meta: { isTouched: boolean; isValid: boolean }) {\n  return meta.isTouched && !meta.isValid;\n}\n"},{"path":"lib/motion.ts","type":"registry:lib","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/use-controllable-state\";\nimport type { MotionProps, Transition } from \"motion/react\";\n\nexport const chewyEase = [0.32, 0.72, 0, 1] as const;\n\nexport const growTransition = { duration: 0.6, ease: chewyEase };\n\nconst springIn: Transition = { type: \"spring\", bounce: 0.2, duration: 0.3 };\nconst quickOut: Transition = { duration: 0.12, ease: \"easeIn\" };\n\nexport const popMotion = {\n  initial: { opacity: 0, scale: 0.96 },\n  animate: { opacity: 1, scale: 1, transition: springIn },\n  exit: { opacity: 0, scale: 0.96, transition: quickOut },\n};\n\nexport const tooltipMotion = {\n  initial: { opacity: 0, scale: 0.94 },\n  animate: { opacity: 1, scale: 1, transition: { ...springIn, duration: 0.2 } },\n  exit: { opacity: 0, scale: 0.94, transition: { duration: 0.08 } },\n};\n\nexport const dialogMotion = {\n  initial: { opacity: 0, scale: 0.95, y: 8 },\n  animate: { opacity: 1, scale: 1, y: 0, transition: { type: \"spring\", bounce: 0.25, duration: 0.4 } as Transition },\n  exit: { opacity: 0, scale: 0.97, y: 4, transition: { duration: 0.15, ease: \"easeIn\" } as Transition },\n};\n\nexport const overlayMotion = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: 0.2 } },\n  exit: { opacity: 0, transition: { duration: 0.15 } },\n};\n\nexport type PanelSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport function panelMotion(side: PanelSide): Pick<MotionProps, \"initial\" | \"animate\" | \"exit\"> {\n  const offscreen = side === \"left\" || side === \"top\" ? \"calc(-100% - 1rem)\" : \"calc(100% + 1rem)\";\n  const horizontal = side === \"left\" || side === \"right\";\n  const hidden = horizontal ? { x: offscreen } : { y: offscreen };\n  const shown = horizontal ? { x: 0 } : { y: 0 };\n\n  return {\n    initial: hidden,\n    animate: { ...shown, transition: { duration: 0.45, ease: chewyEase } },\n    exit: { ...hidden, transition: { duration: 0.3, ease: chewyEase } },\n  };\n}\n\nexport function useAnimatedOpen(\n  openProp: boolean | undefined,\n  defaultOpen: boolean | undefined,\n  onOpenChange: ((open: boolean) => void) | undefined,\n) {\n  const [open, setOpen] = useControllableState(openProp, defaultOpen ?? false, onOpenChange);\n  const [exiting, setExiting] = React.useState(false);\n  const [wasOpen, setWasOpen] = React.useState(open);\n\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    setExiting(!open);\n  }\n\n  const onExitComplete = React.useCallback(() => setExiting(false), []);\n\n  return { open, setOpen, radixOpen: open || exiting, onExitComplete };\n}\n\ntype AnimatedOpenState = { open: boolean; onExitComplete: () => void };\n\ntype OpenProps = { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void };\n\nexport function createAnimatedRoot<P extends OpenProps>(Primitive: React.ComponentType<P>, slot: string) {\n  const Context = React.createContext<AnimatedOpenState>({ open: false, onExitComplete: () => {} });\n\n  function AnimatedRoot({ open: openProp, defaultOpen, onOpenChange, ...props }: P) {\n    const { open, setOpen, radixOpen, onExitComplete } = useAnimatedOpen(openProp, defaultOpen, onOpenChange);\n    const value = React.useMemo(() => ({ open, onExitComplete }), [open, onExitComplete]);\n    const primitiveProps = { \"data-slot\": slot, ...props, open: radixOpen, onOpenChange: setOpen } as unknown as P;\n\n    return React.createElement(Context.Provider, { value }, React.createElement(Primitive, primitiveProps));\n  }\n\n  return [AnimatedRoot, () => React.useContext(Context)] as const;\n}\n"},{"path":"hooks/use-controllable-state.ts","type":"registry:hook","content":"import * as React from \"react\";\n\nexport function useControllableState<T>(prop: T | undefined, defaultProp: T, onChange?: (value: T) => void) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultProp);\n  const isControlled = prop !== undefined;\n  const value = isControlled ? prop : uncontrolled;\n\n  const setValue = React.useCallback(\n    (next: T) => {\n      if (!isControlled) setUncontrolled(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n\n  return [value, setValue] as const;\n}\n"}],"cssVars":{"theme":{"shadow-float":"0 16px 36px -12px oklch(0 0 0 / 0.2), 0 2px 6px -2px oklch(0 0 0 / 0.08)"}},"css":{"@utility field-halo":{"border-color":"color-mix(in oklab, var(--foreground) 40%, transparent)","box-shadow":"0 0 0 3px color-mix(in oklab, var(--foreground) 9%, transparent)"},"@utility eyebrow":{"font-size":"11px","font-weight":"600","letter-spacing":"0.05em","text-transform":"uppercase"},"@utility frosted":{"background-color":"color-mix(in oklab, var(--popover) 65%, transparent)","backdrop-filter":"blur(20px) saturate(1.6)"}}}