{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-input",
  "type": "registry:ui",
  "title": "Date Input",
  "description": "날짜를 연·월·일 세그먼트로 나눠 키보드로 타이핑해 넣는 입력 필드입니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/icons",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-context",
    "@atom/lib-date",
    "@atom/lib-field-context",
    "@atom/lib-use-anchored-position",
    "@atom/lib-use-controllable-state",
    "@atom/lib-use-dismiss"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/date-input.tsx",
      "content": "\"use client\";\n\nimport {\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n  type Ref,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { createStrictContext } from \"../lib/context\";\nimport {\n  addDays,\n  addMonths,\n  dateParts,\n  endOfWeek,\n  firstOfMonth,\n  formatLong,\n  formatMonthTitle,\n  isValidISODate,\n  monthGrid,\n  sameMonth,\n  startOfWeek,\n  todayISO,\n  weekdayLabels,\n} from \"../lib/date\";\nimport { useFieldControl } from \"../lib/field-context\";\nimport { useAnchoredPosition } from \"../lib/use-anchored-position\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\nimport { useDismiss } from \"../lib/use-dismiss\";\nimport { CalendarIcon } from \"./icons\";\n\n/* -------------------------------------------------------------------------- */\n/*  Date Input (components/date-input.md, styles/atom/elements/date-input.css) */\n/*    <DateInput aria-label=\"생년월일\" defaultValue=\"1994-03-11\" />            */\n/*    <DateInput aria-label=\"숙박 기간\" range format=\"YYYY-MM-DD\" />           */\n/*    <DateInput aria-label=\"예약일\">                                         */\n/*      <DateInput.Footer>…프리셋…</DateInput.Footer>                         */\n/*    </DateInput>                                                            */\n/* -------------------------------------------------------------------------- */\n\nexport type DateInputSize = \"small\" | \"medium\" | \"large\";\nexport type DateInputFormat = \"YYYY-MM-DD\" | \"YYYY.MM.DD\" | \"DD/MM/YYYY\";\nexport type DateInputSegment = \"year\" | \"month\" | \"day\";\n/** range 에서 어느 쪽 그룹인지 */\nexport type DateInputRangePart = \"start\" | \"end\";\n\nconst B = \"atom-date-input\";\n\ntype Parts = { year: number | null; month: number | null; day: number | null };\nconst EMPTY: Parts = { year: null, month: null, day: null };\n\nconst FORMATS: Record<DateInputFormat, { order: DateInputSegment[]; sep: string }> = {\n  \"YYYY-MM-DD\": { order: [\"year\", \"month\", \"day\"], sep: \"-\" },\n  \"YYYY.MM.DD\": { order: [\"year\", \"month\", \"day\"], sep: \".\" },\n  \"DD/MM/YYYY\": { order: [\"day\", \"month\", \"year\"], sep: \"/\" },\n};\nconst SEG_LABEL: Record<DateInputSegment, string> = { year: \"연\", month: \"월\", day: \"일\" };\nconst SEG_SUFFIX: Record<DateInputSegment, string> = { year: \"년\", month: \"월\", day: \"일\" };\nconst PLACEHOLDER: Record<DateInputSegment, string> = { year: \"YYYY\", month: \"MM\", day: \"DD\" };\nconst SEPARATOR_KEYS = new Set([\".\", \"-\", \"/\"]);\n\n/* --------------------------------- helpers -------------------------------- */\n\nconst pad = (n: number, len: number) => String(n).padStart(len, \"0\");\n\nfunction toISO(p: Parts): string | null {\n  if (p.year === null || p.month === null || p.day === null) return null;\n  const iso = `${pad(p.year, 4)}-${pad(p.month, 2)}-${pad(p.day, 2)}`;\n  return isValidISODate(iso) ? iso : null;\n}\n\nconst isComplete = (p: Parts) => p.year !== null && p.month !== null && p.day !== null;\n\nfunction partsOf(iso: string | null | undefined): Parts {\n  if (!iso || !isValidISODate(iso)) return EMPTY;\n  const { year, month, day } = dateParts(iso);\n  return { year, month, day };\n}\n\n/** range 값은 ISO 8601 구간 표기 \"YYYY-MM-DD/YYYY-MM-DD\" */\nfunction parseValue(value: string | null | undefined, range: boolean): Parts[] {\n  if (!range) return [partsOf(value)];\n  const [start, end] = (value ?? \"\").split(\"/\");\n  return [partsOf(start), partsOf(end)];\n}\n\nfunction composeValue(drafts: Parts[], range: boolean): string | null {\n  if (!range) return toISO(drafts[0]!);\n  const start = toISO(drafts[0]!);\n  const end = toISO(drafts[1]!);\n  return start && end ? `${start}/${end}` : null;\n}\n\nfunction daysIn(year: number | null, month: number | null) {\n  if (month === null) return 31;\n  return new Date(Date.UTC(year ?? 2000, month, 0)).getUTCDate(); // 연도를 모르면 윤년 기준(29일)\n}\n\n/* --------------------------------- context -------------------------------- */\n\nconst [DateInputContext, useDateInputContext] = createStrictContext<{ close: () => void }>(\"DateInput\");\n\n/* ---------------------------------- props --------------------------------- */\n\nexport type DateInputProps = Omit<HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 선택된 날짜입니다. 화면 표기와 무관하게 항상 `YYYY-MM-DD` 로 주고받습니다. range 는 `YYYY-MM-DD/YYYY-MM-DD`. */\n  value?: string | null;\n  /** 비제어로 쓸 때의 처음 값입니다. */\n  defaultValue?: string | null;\n  /** 세그먼트 순서와 구분자 글리프를 정합니다. 로케일에서 뽑는 것을 권합니다. */\n  format?: DateInputFormat;\n  /** Trigger 와 Calendar popover 를 답니다. `false` 면 타이핑 전용입니다. */\n  showCalendar?: boolean;\n  /** 팝오버에 나란히 펼 달의 수입니다. `range` 에서는 2가 기본입니다. */\n  numberOfMonths?: number;\n  /** 이웃 달의 날짜를 흐리게 남겨 주를 사각형으로 유지합니다. */\n  showOutsideDays?: boolean;\n  /** 고를 수 없는 날짜를 정합니다. (date: \"YYYY-MM-DD\") */\n  disabledDates?: (date: string) => boolean;\n  /** 높이와 좌우 여백입니다. Text Input 과 같은 값입니다. 생략하면 Field 의 size, 없으면 \"medium\". */\n  size?: DateInputSize;\n  /** 한 상자에 시작 · 종료 두 세그먼트 그룹을 둡니다. */\n  range?: boolean;\n  /** 오른쪽 끝에 달력 버튼을 그립니다. `false` 면 타이핑만 받습니다. */\n  withTrigger?: boolean;\n  /** 허용 최소 날짜입니다. 세그먼트의 `aria-valuemin` 계산에도 쓰입니다. */\n  min?: string;\n  /** 허용 최대 날짜입니다. */\n  max?: string;\n  /** 감춘 `input` 의 `name` 입니다. 폼 제출 키입니다. */\n  name?: string;\n  /** 필수 여부입니다. 별표 표시는 Field 가 그립니다. */\n  required?: boolean;\n  /** `[data-invalid]` 를 붙입니다. 오류 문장은 Field 에 함께 두세요. */\n  invalid?: boolean;\n  /** 값은 읽히지만 바꿀 수 없습니다. `[data-readonly]` 로 나갑니다. */\n  readOnly?: boolean;\n  /** `[data-disabled]` 를 붙이고 세그먼트 · 트리거의 포인터를 막습니다. */\n  disabled?: boolean;\n  /** 세 세그먼트가 모두 채워져 유효한 날짜가 됐을 때 호출됩니다. (완성됐던 값이 지워지면 null) */\n  onChange?: (value: string | null) => void;\n  /** 세그먼트 하나가 바뀔 때마다 호출됩니다. 부분 입력을 추적할 때 씁니다. */\n  onSegmentChange?: (segment: DateInputSegment, value: number | null, part: DateInputRangePart) => void;\n  /** 달력 버튼을 눌렀을 때입니다. Date Picker 를 여는 것은 이 콜백을 받는 쪽의 일입니다. */\n  onTriggerClick?: () => void;\n  /** Calendar popover 아래에 붙는 내용 (`<DateInput.Footer>`) */\n  children?: ReactNode;\n};\n\n/* ---------------------------------- root ---------------------------------- */\n\nfunction DateInputRoot({\n  ref,\n  value: valueProp,\n  defaultValue = null,\n  format = \"YYYY.MM.DD\",\n  showCalendar = true,\n  numberOfMonths,\n  showOutsideDays = true,\n  disabledDates,\n  size,\n  range = false,\n  withTrigger = true,\n  min,\n  max,\n  name,\n  required = false,\n  invalid = false,\n  readOnly = false,\n  disabled = false,\n  onChange,\n  onSegmentChange,\n  onTriggerClick,\n  className,\n  children,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  \"aria-describedby\": ariaDescribedBy,\n  ...rest\n}: DateInputProps) {\n  const field = useFieldControl();\n  const isDisabled = disabled || !!field?.disabled;\n  const isRequired = required || !!field?.required;\n  const resolvedSize = size ?? field?.size ?? \"medium\";\n  const labelledBy = ariaLabelledBy ?? (ariaLabel ? undefined : field?.labelId);\n  const describedBy = [field?.describedBy, ariaDescribedBy].filter(Boolean).join(\" \") || undefined;\n\n  const [value, setValue] = useControllableState<string | null>({ value: valueProp, defaultValue, onChange });\n  const [drafts, setDrafts] = useState<Parts[]>(() => parseValue(value, range));\n  const composed = composeValue(drafts, range);\n\n  // value 가 밖에서 바뀌면 세그먼트를 다시 맞춤 (부분 입력 중 null 이 돌아오는 경우는 유지)\n  const [seenValue, setSeenValue] = useState(value);\n  if (value !== seenValue) {\n    setSeenValue(value);\n    if (value !== composed) setDrafts(parseValue(value, range));\n  }\n\n  const { order, sep } = FORMATS[format];\n  const groups = range ? [0, 1] : [0];\n  const flat = groups.flatMap((g) => order.map((seg) => ({ g, seg })));\n\n  const yearMin = min && isValidISODate(min) ? dateParts(min).year : 1900;\n  const yearMax = max && isValidISODate(max) ? dateParts(max).year : 2100;\n  const bounds = (seg: DateInputSegment, p: Parts): [number, number] =>\n    seg === \"year\" ? [yearMin, yearMax] : seg === \"month\" ? [1, 12] : [1, daysIn(p.year, p.month)];\n\n  const outOfBounds = (iso: string) => (min ? iso < min : false) || (max ? iso > max : false);\n  const autoInvalid =\n    drafts.some((p) => {\n      if (!isComplete(p)) return false;\n      const iso = toISO(p);\n      return iso === null || outOfBounds(iso);\n    }) ||\n    (range && !!toISO(drafts[0]!) && !!toISO(drafts[1]!) && toISO(drafts[0]!)! > toISO(drafts[1]!)!);\n  const isInvalid = invalid || !!field?.invalid || autoInvalid;\n\n  /* ------------------------------ segment state ----------------------------- */\n  const segRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const [tabStop, setTabStop] = useState(0);\n  const typingRef = useRef<{ index: number; text: string } | null>(null);\n  const [typing, setTypingState] = useState<{ index: number; text: string } | null>(null);\n  const setTyping = (t: { index: number; text: string } | null) => {\n    typingRef.current = t;\n    setTypingState(t);\n  };\n\n  const commitDrafts = (next: Parts[]) => {\n    setDrafts(next);\n    const nextValue = composeValue(next, range);\n    if (nextValue !== composed) setValue(nextValue);\n  };\n\n  const update = (g: number, seg: DateInputSegment, v: number | null) => {\n    if (drafts[g]![seg] === v) return;\n    commitDrafts(drafts.map((p, i) => (i === g ? { ...p, [seg]: v } : p)));\n    onSegmentChange?.(seg, v, g === 0 ? \"start\" : \"end\");\n  };\n\n  const focusAt = (index: number) => {\n    const clamped = Math.max(0, Math.min(flat.length - 1, index));\n    segRefs.current[clamped]?.focus();\n  };\n\n  const typeDigit = (index: number, digit: string) => {\n    const { g, seg } = flat[index]!;\n    const prev = typingRef.current?.index === index ? typingRef.current.text : \"\";\n    if (seg === \"year\") {\n      const text = prev.length >= 4 ? digit : prev + digit;\n      if (text.length === 4) {\n        setTyping(null);\n        update(g, \"year\", Number(text));\n        focusAt(index + 1);\n      } else {\n        setTyping({ index, text });\n      }\n      return;\n    }\n    const [, hi] = bounds(seg, drafts[g]!);\n    let text = prev + digit;\n    if (text.length > 2 || Number(text) > hi) text = digit;\n    const n = Number(text);\n    if (n === 0) {\n      // \"0\" 다음 자리를 기다림 (08)\n      setTyping({ index, text });\n      update(g, seg, null);\n      return;\n    }\n    update(g, seg, n);\n    // 자동 진행: 한 자리 더 넣어 유효한 값이 있으면 대기, 없으면 바로 진행\n    if (text.length === 2 || n * 10 > hi) {\n      setTyping(null);\n      focusAt(index + 1);\n    } else {\n      setTyping({ index, text });\n    }\n  };\n\n  const stepSegment = (index: number, dir: 1 | -1) => {\n    const { g, seg } = flat[index]!;\n    const [lo, hi] = bounds(seg, drafts[g]!);\n    const cur = drafts[g]![seg];\n    let next: number;\n    if (cur === null) {\n      next = seg === \"year\" ? Math.min(hi, Math.max(lo, dateParts(todayISO()).year)) : dir > 0 ? lo : hi;\n    } else {\n      next = cur + dir;\n      if (next > hi) next = lo;\n      if (next < lo) next = hi;\n    }\n    update(g, seg, next);\n  };\n\n  const onSegmentKeyDown = (index: number, e: KeyboardEvent<HTMLSpanElement>) => {\n    const { g, seg } = flat[index]!;\n    const key = e.key;\n    if (key === \"ArrowLeft\") {\n      e.preventDefault();\n      focusAt(index - 1);\n      return;\n    }\n    if (key === \"ArrowRight\") {\n      e.preventDefault();\n      focusAt(index + 1);\n      return;\n    }\n    if (key === \"ArrowDown\" && e.altKey && canOpen) {\n      e.preventDefault();\n      openCalendar();\n      return;\n    }\n    if (readOnly || isDisabled) return;\n    if (/^\\d$/.test(key)) {\n      e.preventDefault();\n      typeDigit(index, key);\n    } else if (key === \"ArrowUp\" || key === \"ArrowDown\") {\n      e.preventDefault();\n      setTyping(null);\n      stepSegment(index, key === \"ArrowUp\" ? 1 : -1);\n    } else if (key === \"Home\" || key === \"End\") {\n      e.preventDefault();\n      setTyping(null);\n      const [lo, hi] = bounds(seg, drafts[g]!);\n      update(g, seg, key === \"Home\" ? lo : hi);\n    } else if (key === \"Backspace\" || key === \"Delete\") {\n      e.preventDefault();\n      const hasValue = drafts[g]![seg] !== null || typingRef.current?.index === index;\n      setTyping(null);\n      if (hasValue) update(g, seg, null);\n      else if (key === \"Backspace\") focusAt(index - 1);\n    } else if (SEPARATOR_KEYS.has(key)) {\n      e.preventDefault();\n      if (drafts[g]![seg] !== null || typingRef.current?.index === index) focusAt(index + 1);\n    }\n  };\n\n  const onSegmentBlur = (index: number) => {\n    const t = typingRef.current;\n    if (!t || t.index !== index) return;\n    setTyping(null);\n    const { g, seg } = flat[index]!;\n    if (seg === \"year\") update(g, \"year\", Number(t.text)); // 4자리 전에 떠나면 친 만큼만 확정\n  };\n\n  /* -------------------------------- calendar -------------------------------- */\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const triggerRef = useRef<HTMLButtonElement | null>(null);\n  const popoverRef = useRef<HTMLDivElement | null>(null);\n  const popoverId = useId();\n  const [open, setOpen] = useState(false);\n  const months = Math.max(1, numberOfMonths ?? (range ? 2 : 1));\n  const [view, setView] = useState(() => firstOfMonth(todayISO()));\n  const [focusDate, setFocusDate] = useState(() => todayISO());\n  const [focusRequest, setFocusRequest] = useState(0);\n\n  const canOpen = showCalendar && !readOnly && !isDisabled;\n  const hasTrigger = withTrigger && (showCalendar || !!onTriggerClick);\n\n  const isDayDisabled = (iso: string) => outOfBounds(iso) || !!disabledDates?.(iso);\n  const clampToBounds = (iso: string) => (min && iso < min ? min : max && iso > max ? max : iso);\n\n  const showDate = (iso: string) => {\n    setFocusDate(iso);\n    const first = firstOfMonth(iso);\n    const last = addMonths(view, months - 1);\n    if (first < view) setView(first);\n    else if (first > last) setView(addMonths(first, -(months - 1)));\n    setFocusRequest((n) => n + 1);\n  };\n\n  const openCalendar = () => {\n    const start = toISO(drafts[0]!);\n    const end = range ? toISO(drafts[1]!) : null;\n    const anchor = clampToBounds(start ?? end ?? todayISO());\n    setView(firstOfMonth(anchor));\n    setFocusDate(anchor);\n    setFocusRequest((n) => n + 1);\n    setOpen(true);\n  };\n\n  const close = (focusTrigger: boolean) => {\n    setOpen(false);\n    if (focusTrigger) triggerRef.current?.focus();\n  };\n\n  useDismiss(open, [rootRef], (reason) => close(reason === \"escape\"));\n\n  // 위치 · 위로 열기 · 끝 맞춤(원본 --align_end 역할)은 useAnchoredPosition 이 처리\n  useAnchoredPosition(open, rootRef, popoverRef);\n\n  // 키보드로 옮긴 날짜에 초점 (roving tabindex)\n  useEffect(() => {\n    if (!open || focusRequest === 0) return;\n    const pop = popoverRef.current;\n    const btn =\n      pop?.querySelector<HTMLButtonElement>(`[data-date=\"${focusDate}\"]:not([data-outside])`) ??\n      pop?.querySelector<HTMLButtonElement>(`[data-date=\"${focusDate}\"]`);\n    btn?.focus();\n  }, [open, focusRequest, focusDate]);\n\n  const selectDate = (iso: string) => {\n    if (isDayDisabled(iso) || readOnly || isDisabled) return;\n    const picked = partsOf(iso);\n    if (!range) {\n      commitDrafts([picked]);\n      close(true);\n      return;\n    }\n    const start = toISO(drafts[0]!);\n    const end = toISO(drafts[1]!);\n    if (!start || end || iso < start) {\n      // 시작일부터 다시 고름\n      commitDrafts([picked, EMPTY]);\n      setFocusDate(iso);\n      return;\n    }\n    commitDrafts([drafts[0]!, picked]);\n    close(true);\n  };\n\n  const onGridKeyDown = (e: KeyboardEvent<HTMLTableElement>) => {\n    const map: Record<string, () => string> = {\n      ArrowLeft: () => addDays(focusDate, -1),\n      ArrowRight: () => addDays(focusDate, 1),\n      ArrowUp: () => addDays(focusDate, -7),\n      ArrowDown: () => addDays(focusDate, 7),\n      PageUp: () => addMonths(focusDate, e.shiftKey ? -12 : -1),\n      PageDown: () => addMonths(focusDate, e.shiftKey ? 12 : 1),\n      Home: () => startOfWeek(focusDate, 0),\n      End: () => endOfWeek(focusDate, 0),\n    };\n    const move = map[e.key];\n    if (move) {\n      e.preventDefault();\n      showDate(move());\n    } else if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault();\n      selectDate(focusDate);\n    }\n  };\n\n  const today = todayISO();\n  const lastVisible = addMonths(view, months - 1);\n  const tabbableDay = focusDate >= view && firstOfMonth(focusDate) <= lastVisible ? focusDate : view;\n  const rangeStart = range ? toISO(drafts[0]!) : null;\n  const rangeEnd = range ? toISO(drafts[1]!) : null;\n  const single = range ? null : toISO(drafts[0]!);\n  const prevDisabled = !!min && addMonths(view, -1) < firstOfMonth(min);\n  const nextDisabled = !!max && addMonths(lastVisible, 1) > max;\n\n  const renderMonth = (monthISO: string) => (\n    <div key={monthISO} className={`${B}__month`}>\n      <div className={`${B}__caption`}>\n        <span className={`${B}__nav`}>\n          <button\n            className={`${B}__nav-btn`}\n            type=\"button\"\n            aria-label=\"이전 달\"\n            disabled={prevDisabled}\n            onClick={() => setView(addMonths(view, -1))}\n          >\n            <NavPrevIcon />\n          </button>\n        </span>\n        <span className={`${B}__caption-label`} aria-live=\"polite\">\n          {formatMonthTitle(monthISO)}\n        </span>\n        <span className={`${B}__nav`}>\n          <button\n            className={`${B}__nav-btn`}\n            type=\"button\"\n            aria-label=\"다음 달\"\n            disabled={nextDisabled}\n            onClick={() => setView(addMonths(view, 1))}\n          >\n            <NavNextIcon />\n          </button>\n        </span>\n      </div>\n      <table className={`${B}__grid`} role=\"grid\" aria-label={formatMonthTitle(monthISO)} onKeyDown={onGridKeyDown}>\n        <thead className={`${B}__weekdays`}>\n          <tr>\n            {weekdayLabels(0).map((w) => (\n              <th key={w} className={`${B}__weekday`} scope=\"col\" abbr={`${w}요일`}>\n                {w}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {monthGrid(monthISO, 0).map((week) => (\n            <tr key={week[0]} className={`${B}__week`}>\n              {week.map((iso) => {\n                const outside = !sameMonth(iso, monthISO);\n                if (outside && !showOutsideDays) return <td key={iso} role=\"gridcell\" />;\n                const inRange = !outside && !!rangeStart && !!rangeEnd && rangeStart <= rangeEnd;\n                const isStart = inRange && iso === rangeStart;\n                const isEnd = inRange && iso === rangeEnd;\n                const isMiddle = inRange && iso > rangeStart! && iso < rangeEnd!;\n                const selected = !outside && (iso === single || iso === rangeStart || iso === rangeEnd);\n                return (\n                  <td key={iso} role=\"gridcell\">\n                    <button\n                      className={`${B}__day`}\n                      type=\"button\"\n                      data-date={iso}\n                      tabIndex={iso === tabbableDay && !outside ? 0 : -1}\n                      aria-label={formatLong(iso)}\n                      aria-selected={selected ? \"true\" : undefined}\n                      aria-disabled={isDayDisabled(iso) ? \"true\" : undefined}\n                      data-today={iso === today ? \"\" : undefined}\n                      data-outside={outside ? \"\" : undefined}\n                      data-range_start={isStart ? \"\" : undefined}\n                      data-range_middle={isMiddle ? \"\" : undefined}\n                      data-range_end={isEnd ? \"\" : undefined}\n                      onClick={() => {\n                        setFocusDate(iso);\n                        selectDate(iso);\n                      }}\n                    >\n                      {dateParts(iso).day}\n                    </button>\n                  </td>\n                );\n              })}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n\n  /* --------------------------------- render --------------------------------- */\n  const renderGroup = (g: number) => {\n    const p = drafts[g]!;\n    return (\n      <span\n        key={g}\n        className={`${B}__segments`}\n        role=\"group\"\n        aria-label={range ? (g === 0 ? \"시작일\" : \"종료일\") : ariaLabel}\n        aria-labelledby={range ? undefined : labelledBy}\n        aria-describedby={range ? undefined : describedBy}\n        aria-invalid={!range && isInvalid ? true : undefined}\n      >\n        {order.map((seg, si) => {\n          const index = g * order.length + si;\n          const v = p[seg];\n          const typed = typing?.index === index ? typing.text : null;\n          const empty = v === null && typed === null;\n          const [lo, hi] = bounds(seg, p);\n          const nextSeg = order[si + 1];\n          const sepEmpty = nextSeg !== undefined && empty && p[nextSeg] === null && typing?.index !== index + 1;\n          return [\n            <span\n              key={seg}\n              ref={(el) => {\n                segRefs.current[index] = el;\n              }}\n              id={index === 0 ? field?.controlId : undefined}\n              className={`${B}__segment`}\n              role=\"spinbutton\"\n              tabIndex={isDisabled ? -1 : index === tabStop ? 0 : -1}\n              aria-label={SEG_LABEL[seg]}\n              aria-valuenow={v ?? undefined}\n              aria-valuemin={lo}\n              aria-valuemax={hi}\n              aria-valuetext={v !== null ? `${v}${SEG_SUFFIX[seg]}` : undefined}\n              aria-invalid={isInvalid || undefined}\n              aria-required={isRequired || undefined}\n              aria-readonly={readOnly || undefined}\n              aria-disabled={isDisabled || undefined}\n              data-empty={empty ? \"\" : undefined}\n              onFocus={() => setTabStop(index)}\n              onBlur={() => onSegmentBlur(index)}\n              onKeyDown={(e) => onSegmentKeyDown(index, e)}\n            >\n              {typed ?? (v === null ? PLACEHOLDER[seg] : pad(v, seg === \"year\" ? 4 : 2))}\n            </span>,\n            nextSeg !== undefined && (\n              <span key={`${seg}-sep`} className={cn(`${B}__separator`, sepEmpty && `${B}__separator--empty`)}>\n                {sep}\n              </span>\n            ),\n          ];\n        })}\n      </span>\n    );\n  };\n\n  const submitValue = composeValue(drafts, range) ?? \"\";\n  // 값이 완성되면 전체 날짜를 한 번 알림 (세그먼트를 따로 읽는 것만으로는 전체를 확인할 수 없음)\n  const announcement = range\n    ? rangeStart && rangeEnd\n      ? `${formatLong(rangeStart)}부터 ${formatLong(rangeEnd)}까지`\n      : \"\"\n    : single\n      ? formatLong(single)\n      : \"\";\n\n  return (\n    <DateInputContext value={{ close: () => close(true) }}>\n      <div\n        {...rest}\n        ref={(el) => {\n          rootRef.current = el;\n          if (typeof ref === \"function\") ref(el);\n          else if (ref) ref.current = el;\n        }}\n        className={cn(B, mod(B, \"size\", resolvedSize, \"medium\"), range && `${B}--range`, className)}\n        role={range ? \"group\" : undefined}\n        aria-label={range ? ariaLabel : undefined}\n        aria-labelledby={range ? labelledBy : undefined}\n        aria-describedby={range ? describedBy : undefined}\n        data-invalid={isInvalid ? \"\" : undefined}\n        data-readonly={readOnly ? \"\" : undefined}\n        data-disabled={isDisabled ? \"\" : undefined}\n      >\n        {renderGroup(0)}\n        {range && (\n          <span className={`${B}__dash`} aria-hidden=\"true\">\n            ~\n          </span>\n        )}\n        {range && renderGroup(1)}\n        <input\n          className={`${B}__value`}\n          type=\"text\"\n          name={name}\n          value={submitValue}\n          readOnly\n          required={isRequired}\n          disabled={isDisabled}\n          aria-hidden=\"true\"\n          tabIndex={-1}\n        />\n        <span className=\"atom-sr-only\" aria-live=\"polite\">\n          {announcement}\n        </span>\n        {hasTrigger && (\n          <button\n            ref={triggerRef}\n            className={`${B}__trigger`}\n            type=\"button\"\n            aria-label=\"달력 열기\"\n            aria-haspopup={showCalendar ? \"dialog\" : undefined}\n            aria-expanded={showCalendar ? open : undefined}\n            aria-controls={showCalendar && open ? popoverId : undefined}\n            disabled={isDisabled || readOnly}\n            onClick={() => {\n              onTriggerClick?.();\n              if (!canOpen) return;\n              if (open) close(false);\n              else openCalendar();\n            }}\n          >\n            <CalendarIcon />\n          </button>\n        )}\n        {showCalendar && open && (\n          <div\n            ref={popoverRef}\n            id={popoverId}\n            className={`${B}__popover`}\n            data-state=\"open\"\n            role=\"dialog\"\n            aria-label=\"날짜 선택\"\n          >\n            <div className={cn(`${B}__calendar`, months === 2 && `${B}__calendar--months_2`)}>\n              {Array.from({ length: months }, (_, i) => renderMonth(addMonths(view, i)))}\n            </div>\n            {children}\n          </div>\n        )}\n      </div>\n    </DateInputContext>\n  );\n}\n\n/* ---------------------------------- parts --------------------------------- */\n\nexport type DateInputFooterProps = HTMLAttributes<HTMLDivElement> & { ref?: Ref<HTMLDivElement> };\n\n/** Calendar popover 아래의 프리셋 · 지우기 자리. `<DateInput>` 의 children 으로 둡니다. */\nfunction DateInputFooter({ className, ...rest }: DateInputFooterProps) {\n  useDateInputContext(\"DateInput.Footer\");\n  return <div {...rest} className={cn(`${B}__footer`, className)} />;\n}\n\n/* 원본 데모의 월 이동 화살표 (16×16, 스프라이트에 없는 인라인 SVG) */\nfunction NavPrevIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n      <path d=\"M10 3L5 8l5 5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction NavNextIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n      <path d=\"M6 3l5 5-5 5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nexport const DateInput = Object.assign(DateInputRoot, { Footer: DateInputFooter });\n",
      "type": "registry:ui",
      "target": "components/ui/date-input.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/date-input.css",
      "content": "/* ============================================================================\n   Element — Date Input\n   ----------------------------------------------------------------------------\n   A date typed in, one segment at a time, with an optional calendar popover for\n   the times typing is the wrong tool — picking a Thursday, or a date you know by\n   its neighbours rather than its number.\n\n   Composed the way shadcn/ui builds its Date Picker: a trigger opens a popover\n   and the popover holds a calendar. Typing stays the primary path here, so the\n   calendar is an alternative route to the same value rather than the only one —\n   the segments remain editable whether or not the popover is open.\n\n   The box copies Text Input's shape and its three heights on purpose. The two\n   stand next to each other in the same form, and a couple of px of difference\n   there reads as a bug rather than as a variant.\n\n   The segments are separate elements rather than one masked <input> because\n   each one carries its own role=\"spinbutton\" value and its own focus — a single\n   input can announce a caret position but not \"월, 8, 1에서 12 사이\".\n\n   atom-date-input\n     --size_{small|medium|large}             medium is the default\n     --range                                 two segment groups joined by __dash\n     --focused                               static stand-in for :focus-within\n     [data-invalid]  [data-disabled]  [data-readonly]\n     __segments   role=\"group\"               the YYYY MM DD run\n     __segment    role=\"spinbutton\"          [data-empty]  [data-focused]\n     __separator                             the . - / glyph between segments\n     __dash                                  the ~ between a range's two groups\n     __trigger                               calendar button [aria-expanded]\n     __value                                 hidden native input; submits ISO\n\n     __popover    [data-state=\"open|closed\"]  --align_end\n     __calendar   --months_2                  side-by-side months for a range\n     __month      __caption  __caption-label  __nav  __nav-btn\n     __weekdays   __weekday\n     __grid       __week     __day\n       __day      [aria-selected] [data-today] [data-outside]\n                  [aria-disabled] [data-range_start|_middle|_end]\n     __footer                                 presets or a clear action\n   ========================================================================== */\n\n.atom-date-input {\n  position: relative;\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  width: 100%;\n  min-width: 0;\n  min-height: var(--atom-control-h);\n  padding: var(--atom-x3) var(--atom-x4);\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-bg-layer);\n  box-shadow: var(--atom-control-border);\n  font-size: 15px;\n  line-height: 1.45;\n  color: var(--atom-fg-neutral);\n  transition: box-shadow var(--atom-duration-fast) ease;\n}\n\n/* --- segments ------------------------------------------------------------ */\n/* The group is what a screen reader is told the name of (\"생년월일\"), so it has\n   to be a real element and not just the flex row it happens to be. */\n.atom-date-input__segments {\n  display: flex;\n  align-items: center;\n  flex: 1;\n  min-width: 0;\n  gap: 1px;\n}\n\n/* tabular-nums keeps the box from breathing as digits are typed — proportional\n   figures make `11` narrower than `08` and the separators drift. */\n.atom-date-input__segment {\n  padding: 1px 3px;\n  border-radius: var(--atom-radius-r1);\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral);\n  cursor: text;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n/* An empty segment shows its format (YYYY), not a blank. The placeholder is how\n   the accepted shape gets taught; an error message after the fact is not. */\n.atom-date-input__segment[data-empty] {\n  color: var(--atom-fg-neutral-subtle);\n}\n\n.atom-date-input__segment:hover {\n  background: var(--atom-bg-neutral-weak);\n}\n\n/* The focused segment is a filled chip, not a ring. The field already draws a\n   ring on :focus-within, and two nested rings cannot say which of the three\n   segments the arrow keys are about to change. */\n.atom-date-input__segment[data-focused],\n.atom-date-input__segment:focus,\n.atom-date-input__segment:focus-visible {\n  outline: none;\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n}\n\n.atom-date-input__separator {\n  flex-shrink: 0;\n  padding-inline: 1px;\n  color: var(--atom-fg-neutral-muted);\n  user-select: none;\n}\n\n/* A separator next to two empty segments should not out-shout them. */\n.atom-date-input__separator--empty {\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- range --------------------------------------------------------------- */\n/* Both ends sit in one box so the ring wraps the pair. Two adjacent boxes would\n   ring only the half being typed and lose \"이 둘이 한 값\" . */\n.atom-date-input--range .atom-date-input__segments {\n  flex: 0 0 auto;\n}\n\n.atom-date-input__dash {\n  flex-shrink: 0;\n  padding-inline: var(--atom-x1);\n  color: var(--atom-fg-neutral-muted);\n  user-select: none;\n}\n\n/* --- trigger ------------------------------------------------------------- */\n/* Optional. Typing has to work without it — the trigger only opens a Date\n   Picker, and this file owns none of that logic. */\n.atom-date-input__trigger {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 28px;\n  height: 28px;\n  flex-shrink: 0;\n  margin-inline: auto -4px;\n  border: 0;\n  border-radius: var(--atom-radius-r1);\n  background: none;\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-date-input__trigger svg {\n  width: 24px;\n  height: 24px;\n  display: block;\n}\n\n.atom-date-input__trigger:hover {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-date-input__trigger:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -2px;\n}\n\n/* --- hidden value -------------------------------------------------------- */\n/* Spans cannot be submitted and cannot carry required / disabled / readonly, so\n   the real value rides along in a native input kept out of the accessibility\n   tree — the spinbuttons already announce it. */\n.atom-date-input__value {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  border: 0;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-date-input--size_small {\n  min-height: var(--atom-control-h-small);\n  padding: var(--atom-x2) var(--atom-x3);\n  font-size: 14px;\n}\n\n.atom-date-input--size_small .atom-date-input__trigger {\n  width: 24px;\n  height: 24px;\n}\n\n.atom-date-input--size_large {\n  min-height: var(--atom-control-h-large);\n  padding-inline: var(--atom-x5);\n  border-radius: var(--atom-radius-r3);\n  font-size: 16px;\n}\n\n.atom-date-input--size_large .atom-date-input__trigger {\n  width: 32px;\n  height: 32px;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-date-input:hover:not(.atom-date-input--focused):not([data-invalid]):not(\n    [data-readonly]\n  ):not([data-disabled]) {\n  box-shadow: var(--atom-control-border-hover);\n}\n\n/* The field ring says \"이 필드를 입력 중\", the segment chip says which part.\n   Two separate signals, both needed at the same time. */\n.atom-date-input--focused,\n.atom-date-input:focus-within {\n  box-shadow: var(--atom-control-border-focus);\n}\n\n.atom-date-input[data-invalid],\n.atom-date-input[data-invalid]:focus-within {\n  box-shadow: var(--atom-control-border-invalid);\n}\n\n.atom-date-input[data-readonly] {\n  background: var(--atom-bg-neutral-weak);\n  box-shadow: none;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-date-input[data-readonly] .atom-date-input__segment {\n  cursor: default;\n  color: inherit;\n}\n\n.atom-date-input[data-readonly] .atom-date-input__segment:hover {\n  background: none;\n}\n\n.atom-date-input[data-disabled],\n.atom-date-input:has(.atom-date-input__value[disabled]) {\n  opacity: 0.45;\n  background: var(--atom-bg-neutral-weak);\n  box-shadow: none;\n  cursor: not-allowed;\n}\n\n.atom-date-input[data-disabled] .atom-date-input__segment,\n.atom-date-input[data-disabled] .atom-date-input__trigger {\n  pointer-events: none;\n}\n\n.atom-date-input__trigger[aria-expanded=\"true\"] {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n/* --- popover ------------------------------------------------------------- */\n/* Anchored to the field, which is already `position: relative`. Production code\n   measures the trigger and flips this above the field when the viewport runs\n   out below — a calendar that opens off-screen is worse than no calendar. */\n.atom-date-input__popover {\n  position: absolute;\n  top: calc(100% + 6px);\n  inset-inline-start: 0;\n  z-index: 20;\n  padding: var(--atom-x3);\n  border-radius: var(--atom-radius-r3);\n  background: var(--atom-bg-layer-raised);\n  box-shadow: var(--atom-shadow-s3), inset 0 0 0 1px var(--atom-border-subtle);\n  cursor: default;\n}\n\n.atom-date-input__popover[data-state=\"closed\"] {\n  display: none;\n}\n\n.atom-date-input__popover--align_end {\n  inset-inline: auto 0;\n}\n\n/* --- calendar ------------------------------------------------------------ */\n.atom-date-input__calendar {\n  display: flex;\n  gap: var(--atom-x5);\n  font-size: 13px;\n  color: var(--atom-fg-neutral);\n}\n\n.atom-date-input__month {\n  min-width: 0;\n}\n\n/* --- caption ------------------------------------------------------------- */\n/* The nav sits at both ends and the label in the middle rather than the label\n   leading with the arrows beside it: the month name changes width every month,\n   and buttons that shuffle sideways as the year advances get missed. */\n.atom-date-input__caption {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x2);\n  height: 28px;\n  margin-bottom: var(--atom-x2);\n}\n\n.atom-date-input__caption-label {\n  flex: 1;\n  text-align: center;\n  font-weight: 600;\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n\n.atom-date-input__nav {\n  display: flex;\n  flex-shrink: 0;\n}\n\n.atom-date-input__nav-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 26px;\n  height: 26px;\n  border: 0;\n  border-radius: var(--atom-radius-r1);\n  background: none;\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-date-input__nav-btn svg {\n  width: 15px;\n  height: 15px;\n}\n\n.atom-date-input__nav-btn:hover {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-date-input__nav-btn:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -2px;\n}\n\n.atom-date-input__nav-btn[aria-disabled=\"true\"],\n.atom-date-input__nav-btn[disabled] {\n  opacity: 0.35;\n  pointer-events: none;\n}\n\n/* --- grid ---------------------------------------------------------------- */\n/* A real table. The month is a grid of dates with weekday headers, which is\n   what <table> means — and it is how a screen reader announces \"8월 27일,\n   목요일\" without the markup having to say it twice. */\n.atom-date-input__grid {\n  border-collapse: collapse;\n  table-layout: fixed;\n}\n\n/* The header row and the week rows carry no paint of their own, but they are the\n   rows a screen reader walks and the hooks a weekend or holiday tint would hang\n   off — so they stay named rather than collapsing into bare <tr>. */\n.atom-date-input__weekdays,\n.atom-date-input__week {\n  border: 0;\n}\n\n.atom-date-input__weekday {\n  width: 32px;\n  height: 26px;\n  font-size: 11px;\n  font-weight: 500;\n  color: var(--atom-fg-neutral-subtle);\n  text-align: center;\n}\n\n.atom-date-input__day {\n  width: 32px;\n  height: 32px;\n  padding: 0;\n  border: 0;\n  border-radius: var(--atom-radius-r1);\n  background: none;\n  font: inherit;\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral);\n  text-align: center;\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-date-input__day:hover:not([aria-disabled=\"true\"]):not(\n    [aria-selected=\"true\"]\n  ) {\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-date-input__day:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -2px;\n}\n\n/* Today is a marker, not a selection — a ring rather than a fill, so the two\n   can show up on the same cell without either one winning. */\n.atom-date-input__day[data-today] {\n  box-shadow: inset 0 0 0 1px var(--atom-border-strong);\n  font-weight: 600;\n}\n\n.atom-date-input__day[aria-selected=\"true\"] {\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n  font-weight: 600;\n}\n\n/* Kept in the grid rather than blanked so the weeks stay rectangular; dimmed so\n   the eye still stops at the month boundary. shadcn's `showOutsideDays`. */\n.atom-date-input__day[data-outside] {\n  color: var(--atom-fg-neutral-subtle);\n  opacity: 0.55;\n}\n\n.atom-date-input__day[aria-disabled=\"true\"],\n.atom-date-input__day[disabled] {\n  opacity: 0.3;\n  cursor: not-allowed;\n}\n\n/* --- range --------------------------------------------------------------- */\n/* The middle is a weak fill that squares off its corners, so a run of days\n   reads as one bar with rounded ends instead of a row of separate pills. */\n.atom-date-input__day[data-range_middle] {\n  border-radius: 0;\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n  font-weight: 400;\n}\n\n.atom-date-input__day[data-range_start],\n.atom-date-input__day[data-range_end] {\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n  font-weight: 600;\n}\n\n.atom-date-input__day[data-range_start] {\n  border-radius: var(--atom-radius-r1) 0 0 var(--atom-radius-r1);\n}\n\n.atom-date-input__day[data-range_end] {\n  border-radius: 0 var(--atom-radius-r1) var(--atom-radius-r1) 0;\n}\n\n.atom-date-input__calendar--months_2 .atom-date-input__month +\n  .atom-date-input__month {\n  padding-inline-start: var(--atom-x5);\n  border-inline-start: 1px solid var(--atom-border-subtle);\n}\n\n/* --- footer -------------------------------------------------------------- */\n.atom-date-input__footer {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x2);\n  margin-top: var(--atom-x3);\n  padding-top: var(--atom-x3);\n  border-top: 1px solid var(--atom-border-subtle);\n}\n\n/* Only the ring and the chip animate, but both carry meaning — invalid, and\n   which segment — and a fade is the wrong way to deliver either one. */\n@media (prefers-reduced-motion: reduce) {\n  .atom-date-input,\n  .atom-date-input__segment,\n  .atom-date-input__trigger,\n  .atom-date-input__nav-btn,\n  .atom-date-input__day {\n    transition: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/date-input.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/field.css",
      "content": "/* ============================================================================\n   Element — Field\n   ----------------------------------------------------------------------------\n   The label / description / error / counter wrapper every input-ish component\n   is dropped into. Field owns the surrounding text, never the control itself.\n\n   atom-field\n     --layout_horizontal                     label to the left of the control\n     --size_{small|medium}                   medium is the default\n     [data-invalid]  [data-disabled]\n     __header  __label  __required  __optional\n     __control                               slot for the actual control\n     __separator                             the text between a range's inputs\n     __footer  __description  __error  __counter\n\n   __control is a row, not a single box. One input, an input plus a Button, or\n   two inputs joined by __separator for a range — all of them are the same slot,\n   so the 4px rhythm and the single row height are defined here once instead of\n   in every page that needs a combination.\n   ========================================================================== */\n\n.atom-field {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  width: 100%;\n  min-width: 0;\n}\n\n.atom-field__header {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n}\n\n.atom-field__label {\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 1.4;\n  color: var(--atom-fg-neutral);\n}\n\n.atom-field__required {\n  margin-left: 2px;\n  color: var(--atom-primary-600);\n}\n\n.atom-field__optional {\n  font-size: 12px;\n  font-weight: 400;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- control slot -------------------------------------------------------- */\n/* Members sit 4px apart and share one height. Every control already agrees on\n   that height — `--atom-control-h`, see the Control surface block in\n   tokens.css — so the only member that needs handling is Button, which keeps\n   its own 47px: `stretch` cannot pull a fixed height, so the height is\n   released and the row decides. */\n.atom-field__control {\n  display: flex;\n  align-items: stretch;\n  gap: var(--atom-x1);\n  min-width: 0;\n}\n\n/* Every member starts at `width: 100%`, so no explicit `flex` is needed — they\n   all ask for the full row and shrink together. What must not shrink says so\n   below; what is left over goes to the typing surfaces, and two inputs in a\n   range land on the same width without being told. */\n.atom-field__control > * {\n  min-width: 0;\n}\n\n/* `:not(:only-child)` matters: a Button alone in the slot has nothing taller to\n   stretch against, so releasing its height would collapse it to its own text.\n   The height only gives way once there is a row to match. Every other control\n   is already `--atom-control-h` and needs nothing here. */\n.atom-field__control > .atom-button:not(:only-child) {\n  height: auto;\n  flex-shrink: 0;\n}\n\n\n/* Focus follows the control ring too. base.css draws its ring *outside* the\n   element at a 2px offset, which lands on the neighbour across a 4px gap, so a\n   Button in a combination borrows the inset ring every other control uses. */\n.atom-field__control > .atom-button:not(:only-child):focus-visible {\n  outline: none;\n  box-shadow: var(--atom-control-border-focus);\n}\n\n/* An icon button in the slot is square. Its own `--layout_icon-only` widths\n   (32 / 40 / 47 / 54px) follow Button's heights, and in the slot the row sets\n   the height instead — so the width is re-pointed at the control heights the\n   row can take, picked by the size modifier the author already put on the\n   button to match the control beside it.\n\n   `aspect-ratio` cannot do this: flex resolves an item's width before the row\n   stretches its height, so with `width: auto` there is no definite height for\n   the ratio to work from and the button collapses to its icon (0 wide, once\n   the icon is a percentage of it).\n\n   The icon is a share of the box rather than a fixed size, so it tracks all\n   three heights — 40 → 16px, 48 → 19.2px, 56 → 22.4px — which is the 38–42%\n   band Button's own sizes already sit in. */\n.atom-field__control > .atom-button--layout_icon-only:not(:only-child) {\n  width: var(--atom-control-h);\n  padding-inline: 0;\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_xsmall:not(:only-child),\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_small:not(:only-child) {\n  width: var(--atom-control-h-small);\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only.atom-button--size_large:not(:only-child) {\n  width: var(--atom-control-h-large);\n}\n\n.atom-field__control\n  > .atom-button--layout_icon-only:not(:only-child)\n  .atom-button__icon {\n  width: 40%;\n  /* Not `height: 40%` — the row's height is auto, so a percentage height falls\n     back to the svg's 150px intrinsic height and the row grows to fit it. */\n  height: auto;\n  aspect-ratio: 1;\n}\n\n/* Checkbox, Radio and Switch are a line of text with a marker, not a box: they\n   have no control height, so a slot holding only those is as tall as its text\n   and sits above the 48px inputs in the fields beside it. Give that slot the\n   control height and centre the group in it — the marker then lines up with\n   the text inside those inputs. `:has` because the slot, not the group, is the\n   box being sized. */\n.atom-field__control:has(> .atom-checkbox-group:only-child),\n.atom-field__control:has(> .atom-radio-group:only-child),\n.atom-field__control:has(> .atom-checkbox:only-child),\n.atom-field__control:has(> .atom-radio:only-child),\n.atom-field__control:has(> .atom-switch:only-child) {\n  align-items: center;\n  min-height: var(--atom-control-h);\n}\n\n.atom-field--size_small\n  .atom-field__control:has(> .atom-checkbox-group:only-child),\n.atom-field--size_small\n  .atom-field__control:has(> .atom-radio-group:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-checkbox:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-radio:only-child),\n.atom-field--size_small .atom-field__control:has(> .atom-switch:only-child) {\n  min-height: var(--atom-control-h-small);\n}\n\n/* The ‘~’ or ‘부터’ between a range's two inputs. It is a label, not a control,\n   so it keeps its own size and centres against the row instead of stretching. */\n.atom-field__separator {\n  flex-shrink: 0;\n  align-self: center;\n  padding-inline: 2px;\n  font-size: 15px;\n  color: var(--atom-fg-neutral-muted);\n  white-space: nowrap;\n}\n\n.atom-field__footer {\n  display: flex;\n  align-items: flex-start;\n  justify-content: space-between;\n  gap: var(--atom-x4);\n  font-size: 12px;\n  line-height: 1.5;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-field__description,\n.atom-field__error {\n  min-width: 0;\n  word-break: keep-all;\n}\n\n.atom-field__error {\n  display: flex;\n  align-items: flex-start;\n  gap: 4px;\n  color: var(--atom-fg-critical);\n}\n\n.atom-field__error-icon {\n  width: 13px;\n  height: 13px;\n  flex-shrink: 0;\n  margin-top: 2px;\n}\n\n.atom-field__counter {\n  flex-shrink: 0;\n  margin-left: auto;\n  font-variant-numeric: tabular-nums;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-field--size_small .atom-field__label {\n  font-size: 12px;\n}\n\n.atom-field--size_small .atom-field__footer {\n  font-size: 11px;\n}\n\n/* --- horizontal layout --------------------------------------------------- */\n.atom-field--layout_horizontal {\n  flex-direction: row;\n  align-items: flex-start;\n  gap: var(--atom-x5);\n}\n\n.atom-field--layout_horizontal .atom-field__header {\n  flex-shrink: 0;\n  width: 120px;\n  padding-top: 12px;\n}\n\n.atom-field--layout_horizontal .atom-field__body {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  flex: 1;\n  min-width: 0;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-field[data-invalid] .atom-field__description {\n  color: var(--atom-fg-critical);\n}\n\n.atom-field[data-disabled] {\n  opacity: 0.45;\n}\n\n/* Legacy alias — the old card preview marked invalid with a modifier class. */\n.atom-field--invalid .atom-field__description {\n  color: var(--atom-fg-critical);\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/field.css"
    }
  ],
  "categories": [
    "action-input"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "component",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "DateInput",
      "DateInput.Footer"
    ],
    "docs": "poc/design-system/components/date-input.md"
  }
}
