{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date",
  "title": "DateField",
  "description": "Date and datetime field for BuzzForm v2.",
  "dependencies": [
    "date-fns"
  ],
  "registryDependencies": [
    "@buzzform/init",
    "popover",
    "calendar",
    "input",
    "input-group",
    "field"
  ],
  "files": [
    {
      "path": "registry/shadcn/fields/date.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { DateField as DateFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport {\n  format,\n  parse,\n  isValid,\n  startOfDay,\n  addDays,\n  startOfWeek,\n  addWeeks,\n  setHours,\n  setMinutes,\n  setSeconds,\n  getHours,\n  getMinutes,\n  getSeconds,\n} from \"date-fns\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport { Calendar as BaseCalendar } from \"@/components/ui/calendar\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/components/ui/input-group\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// UI options\n// ---------------------------------------------------------------------------\n\ninterface DatePreset {\n  label: string;\n  value: string | (() => string);\n}\n\ninterface DateUi {\n  /** date-fns display format for the calendar trigger. Defaults to `\"MM/dd/yyyy\"`. */\n  format?: string;\n  /** date-fns format for manual text input. Defaults to `format`. */\n  inputFormat?: string;\n  /** Quick-select presets. `true` uses built-in Today/Tomorrow/Next Week. */\n  presets?: boolean | DatePreset[];\n  /** Time picker config (only used when `field.withTime` is true). */\n  timePicker?:\n    | boolean\n    | {\n        /** Minute interval for the time input step. Defaults to `1`. */\n        interval?: number;\n        /** Use 24-hour format. Defaults to `true`. */\n        use24hr?: boolean;\n        /** Include seconds. Defaults to `false`. */\n        includeSeconds?: boolean;\n      };\n  /** Auto-focus the text input on mount. */\n  autoFocus?: boolean;\n  /** className applied to `<FieldGroup>`. */\n  className?: string;\n  /** Inline width applied to `<FieldGroup>`. */\n  width?: string | number;\n  /** Controls required asterisk visibility. */\n  asterisk?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_PRESETS: DatePreset[] = [\n  { label: \"Today\", value: () => startOfDay(new Date()).toISOString() },\n  {\n    label: \"Tomorrow\",\n    value: () => startOfDay(addDays(new Date(), 1)).toISOString(),\n  },\n  {\n    label: \"Next Week\",\n    value: () =>\n      startOfWeek(addWeeks(new Date(), 1), { weekStartsOn: 1 }).toISOString(),\n  },\n];\n\nfunction isoToDate(iso: string | undefined | null): Date | undefined {\n  if (!iso) return undefined;\n  const d = new Date(iso);\n  return isValid(d) ? d : undefined;\n}\n\nfunction dateToIso(date: Date, withTime: boolean): string {\n  if (withTime) return date.toISOString();\n  return format(date, \"yyyy-MM-dd\");\n}\n\n// ---------------------------------------------------------------------------\n// DateField\n// ---------------------------------------------------------------------------\n\nexport function DateField() {\n  const {\n    fieldApi,\n    field,\n    isDisabled,\n    isReadOnly,\n    isRequired,\n    label,\n    placeholder,\n    description,\n    errors,\n    isInvalid,\n    descriptionId,\n    errorId,\n    ariaDescribedBy,\n    handleBlur,\n  } = useDataField<DateFieldDef>();\n\n  const ui = field.ui as DateUi | undefined;\n  const withTime = field.withTime ?? false;\n  const displayFormat = ui?.format ?? \"MM/dd/yyyy\";\n  const inputFormat = ui?.inputFormat ?? displayFormat;\n\n  const timePickerConfig = withTime ? (ui?.timePicker ?? true) : undefined;\n  const includeSeconds =\n    typeof timePickerConfig === \"object\"\n      ? timePickerConfig.includeSeconds === true\n      : false;\n  const timeInterval =\n    typeof timePickerConfig === \"object\" && timePickerConfig.interval\n      ? timePickerConfig.interval\n      : 1;\n  const timeStep = includeSeconds ? 1 : timeInterval * 60;\n\n  const presetsConfig = ui?.presets;\n  const presets: DatePreset[] | null =\n    presetsConfig === true\n      ? DEFAULT_PRESETS\n      : Array.isArray(presetsConfig)\n        ? presetsConfig\n        : null;\n\n  const minDate = isoToDate(field.minDate);\n  const maxDate = isoToDate(field.maxDate);\n\n  const storedValue = fieldApi.state.value as string | undefined;\n  const dateValue = isoToDate(storedValue);\n\n  const [isOpen, setIsOpen] = React.useState(false);\n  const [inputText, setInputText] = React.useState(() =>\n    dateValue ? format(dateValue, inputFormat) : \"\",\n  );\n  const [lastSynced, setLastSynced] = React.useState<Date | undefined>(\n    dateValue,\n  );\n\n  // Keep inputText in sync when value changes externally\n  if (dateValue?.getTime() !== lastSynced?.getTime()) {\n    setLastSynced(dateValue);\n    const formatted = dateValue ? format(dateValue, inputFormat) : \"\";\n    if (formatted !== inputText) setInputText(formatted);\n  }\n\n  const width = ui?.width;\n  const widthStyle = width\n    ? {\n        width: typeof width === \"number\" ? `${width}px` : width,\n        flex: \"0 1 auto\",\n      }\n    : undefined;\n\n  const clampDate = (date: Date): Date => {\n    if (minDate && date < startOfDay(minDate)) return minDate;\n    if (maxDate && date > startOfDay(maxDate)) return maxDate;\n    return date;\n  };\n\n  const preserveTime = (next: Date): Date => {\n    if (!dateValue || !withTime) return next;\n    let d = setHours(next, getHours(dateValue));\n    d = setMinutes(d, getMinutes(dateValue));\n    d = setSeconds(d, getSeconds(dateValue));\n    return d;\n  };\n\n  const commit = (date: Date) => {\n    fieldApi.handleChange(dateToIso(date, withTime));\n  };\n\n  const handleDateSelect = (date: Date | undefined) => {\n    if (!date) {\n      fieldApi.handleChange(undefined as unknown as string);\n      return;\n    }\n    commit(clampDate(preserveTime(date)));\n    if (!withTime) setIsOpen(false);\n  };\n\n  const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const [h, m, s] = e.target.value.split(\":\").map(Number);\n    const base = dateValue ?? new Date();\n    let next = setHours(base, h || 0);\n    next = setMinutes(next, m || 0);\n    next = setSeconds(next, s || 0);\n    commit(clampDate(next));\n  };\n\n  const handleClear = (e: React.MouseEvent) => {\n    e.stopPropagation();\n    fieldApi.handleChange(undefined as unknown as string);\n  };\n\n  const handlePreset = (preset: DatePreset) => {\n    const iso =\n      typeof preset.value === \"function\" ? preset.value() : preset.value;\n    const date = isoToDate(iso);\n    if (!date) return;\n    commit(clampDate(preserveTime(date)));\n    if (!withTime) setIsOpen(false);\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setInputText(e.target.value);\n  };\n\n  const handleInputBlur = () => {\n    handleBlur();\n    if (!inputText.trim()) {\n      if (!isRequired) fieldApi.handleChange(undefined as unknown as string);\n      return;\n    }\n    const parsed = parse(inputText, inputFormat, new Date());\n    if (isValid(parsed)) {\n      const clamped = clampDate(preserveTime(parsed));\n      if (minDate && parsed < startOfDay(minDate)) {\n        setInputText(dateValue ? format(dateValue, inputFormat) : \"\");\n        return;\n      }\n      if (maxDate && parsed > startOfDay(maxDate)) {\n        setInputText(dateValue ? format(dateValue, inputFormat) : \"\");\n        return;\n      }\n      commit(clamped);\n    } else {\n      setInputText(dateValue ? format(dateValue, inputFormat) : \"\");\n    }\n  };\n\n  const timeValue = dateValue\n    ? includeSeconds\n      ? format(dateValue, \"HH:mm:ss\")\n      : format(dateValue, \"HH:mm\")\n    : \"\";\n\n  const disabledMatchers = [\n    ...(minDate ? [{ before: startOfDay(minDate) }] : []),\n    ...(maxDate ? [{ after: startOfDay(maxDate) }] : []),\n  ];\n\n  const calendarPopover = (\n    <div className=\"flex flex-col\">\n      <BaseCalendar\n        mode=\"single\"\n        selected={dateValue}\n        onSelect={handleDateSelect}\n        disabled={disabledMatchers.length ? disabledMatchers : undefined}\n        autoFocus={ui?.autoFocus}\n        className=\"w-full\"\n      />\n      {presets && presets.length > 0 && (\n        <div className=\"border-t px-3 py-2 flex flex-wrap gap-1.5\">\n          {presets.map((preset, i) => (\n            <InputGroupButton\n              key={i}\n              variant=\"outline\"\n              size=\"sm\"\n              className=\"h-7 text-xs px-2.5 rounded-full\"\n              onClick={() => handlePreset(preset)}\n            >\n              {preset.label}\n            </InputGroupButton>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n\n  const dateInput = (\n    <InputGroup>\n      <InputGroupAddon align=\"inline-start\" className=\"pl-3\">\n        <Popover open={isOpen} onOpenChange={setIsOpen} modal>\n          <PopoverTrigger\n            className=\"h-6 w-6 p-0 inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-muted-foreground\"\n            disabled={isDisabled || isReadOnly}\n          >\n            <IconPlaceholder\n              lucide=\"Calendar\"\n              hugeicons=\"Calendar03Icon\"\n              tabler=\"IconCalendar\"\n              phosphor=\"Calendar\"\n              remixicon=\"RiCalendarLine\"\n              className=\"size-4\"\n            />\n          </PopoverTrigger>\n          <PopoverContent\n            className=\"p-0 w-auto min-w-(--anchor-width)\"\n            align=\"start\"\n          >\n            {calendarPopover}\n          </PopoverContent>\n        </Popover>\n      </InputGroupAddon>\n      <InputGroupInput\n        id={fieldApi.name}\n        value={inputText}\n        onChange={handleInputChange}\n        onBlur={handleInputBlur}\n        placeholder={placeholder || inputFormat.toLowerCase()}\n        disabled={isDisabled}\n        readOnly={isReadOnly}\n        autoComplete={field.autoComplete}\n        autoFocus={ui?.autoFocus}\n        aria-invalid={isInvalid}\n        aria-describedby={ariaDescribedBy}\n        className=\"px-2\"\n      />\n      {!isRequired && !isDisabled && !isReadOnly && dateValue && (\n        <InputGroupAddon align=\"inline-end\">\n          <InputGroupButton\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            onClick={handleClear}\n            title=\"Clear date\"\n          >\n            <IconPlaceholder\n              lucide=\"X\"\n              hugeicons=\"Cancel01Icon\"\n              tabler=\"IconX\"\n              phosphor=\"X\"\n              remixicon=\"RiCloseLine\"\n              className=\"size-3.5 text-muted-foreground\"\n            />\n          </InputGroupButton>\n        </InputGroupAddon>\n      )}\n    </InputGroup>\n  );\n\n  const content = withTime ? (\n    <FieldGroup className=\"flex-col sm:flex-row gap-3 items-stretch\">\n      <div className=\"flex flex-col gap-1.5 flex-1\">\n        <FieldLabel\n          htmlFor={fieldApi.name}\n          className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 px-0.5 mb-0\"\n        >\n          Date\n        </FieldLabel>\n        {dateInput}\n      </div>\n      <div className=\"flex flex-col gap-1.5 w-full sm:w-40\">\n        <FieldLabel\n          htmlFor={`${fieldApi.name}-time`}\n          className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 px-0.5 mb-0\"\n        >\n          Time\n        </FieldLabel>\n        <InputGroup\n          className={cn(!dateValue && \"opacity-50 cursor-not-allowed\")}\n        >\n          <InputGroupAddon\n            align=\"inline-start\"\n            className=\"pl-3 pointer-events-none\"\n          >\n            <IconPlaceholder\n              lucide=\"Clock\"\n              hugeicons=\"Clock01Icon\"\n              tabler=\"IconClock\"\n              phosphor=\"Clock\"\n              remixicon=\"RiTimeLine\"\n              className=\"size-4 shrink-0\"\n            />\n          </InputGroupAddon>\n          <InputGroupInput\n            id={`${fieldApi.name}-time`}\n            type=\"time\"\n            step={timeStep}\n            value={timeValue}\n            onChange={handleTimeChange}\n            disabled={isDisabled || !dateValue}\n            className=\"px-2 focus-visible:ring-0 outline-none appearance-none [&::-webkit-calendar-picker-indicator]:hidden\"\n            placeholder=\"--:--\"\n          />\n        </InputGroup>\n      </div>\n    </FieldGroup>\n  ) : (\n    dateInput\n  );\n\n  return (\n    <FieldGroup\n      data-field={fieldApi.name}\n      className={ui?.className}\n      style={widthStyle}\n    >\n      <Field data-invalid={isInvalid} data-disabled={isDisabled}>\n        {label && (\n          <FieldLabel htmlFor={fieldApi.name} className=\"gap-1 items-baseline\">\n            {label}\n            {isRequired && ui?.asterisk !== false ? (\n              <span className=\"text-destructive\">*</span>\n            ) : null}\n          </FieldLabel>\n        )}\n        <FieldContent>{content}</FieldContent>\n        {description && !isInvalid && (\n          <FieldDescription id={descriptionId}>{description}</FieldDescription>\n        )}\n        {isInvalid && <FieldError id={errorId} errors={errors} />}\n      </Field>\n    </FieldGroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/buzzform/fields/date.tsx"
    }
  ],
  "type": "registry:ui"
}