{
  "$schema": "https://cubixflow.ir/schema/registry-item.json",
  "name": "birthday-date",
  "type": "registry:ui",
  "title": "Birthday Date",
  "description": "A labeled birthday field with separate day, month, and year inputs. Values always display as Persian digits.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/cubix/birthday-date.tsx",
      "type": "registry:ui",
      "target": "components/cubix/birthday-date.tsx",
      "content": "\"use client\"\n\n/*\n  Cubix Birthday Date - labeled day / month / year field specialized from Text Field.\n\n  Three numeric segments with / separators. Values always display as Persian\n  digits. inputMode is locked to numeric. Weight 400 and tracking-normal keep\n  IRANSans XV readable in fixed-height controls.\n*/\nimport * as React from \"react\"\nimport { Field as FieldPrimitive } from \"@base-ui/react/field\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst birthdayDateControlVariants = cva(\n  \"group/birthday-date-control relative flex w-full min-w-0 items-center outline-none\",\n  {\n    variants: {\n      size: {\n        default: \"gap-2\",\n        lg: \"gap-2.5\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n)\n\nconst birthdayDateSegmentVariants = cva(\n  \"w-full min-w-0 flex-1 rounded-lg border border-input bg-transparent text-center font-normal leading-none tracking-normal text-foreground tabular-nums transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/20 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-transparent disabled:bg-muted disabled:text-muted-foreground disabled:placeholder:text-muted-foreground aria-invalid:border-destructive aria-invalid:ring-0 aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-0 dark:bg-input/30 dark:disabled:bg-muted dark:aria-invalid:border-destructive/50 dark:aria-invalid:focus-visible:border-destructive/50\",\n  {\n    variants: {\n      size: {\n        default: \"h-10 px-2 text-label md:text-label leading-none\",\n        lg: \"h-12 px-2.5 text-label md:text-label leading-none\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n)\n\ntype BirthdayDateSize = NonNullable<\n  VariantProps<typeof birthdayDateControlVariants>[\"size\"]\n>\n\ntype BirthdayDatePart = \"day\" | \"month\" | \"year\"\n\ntype BirthdayDateParts = {\n  day: string\n  month: string\n  year: string\n}\n\ntype BirthdayDateStore = {\n  parts: BirthdayDateParts\n  setPart: (part: BirthdayDatePart, digits: string) => void\n  setParts: (parts: BirthdayDateParts) => void\n}\n\nconst BirthdayDateSizeContext = React.createContext<BirthdayDateSize>(\"default\")\nconst BirthdayDateStoreContext = React.createContext<BirthdayDateStore | null>(\n  null\n)\n\nconst PERSIAN_DIGITS = \"۰۱۲۳۴۵۶۷۸۹\"\nconst ARABIC_DIGITS = \"٠١٢٣٤٥٦٧٨٩\"\nconst DIGIT_CHAR = /[\\d۰-۹٠-٩]/\nconst ALLOWED_INSERT = /^[\\d۰-۹٠-٩]+$/\nconst MAX_LENGTH: Record<BirthdayDatePart, number> = {\n  day: 2,\n  month: 2,\n  year: 4,\n}\nconst PART_ORDER: BirthdayDatePart[] = [\"day\", \"month\", \"year\"]\nconst PART_SLOT: Record<BirthdayDatePart, string> = {\n  day: \"birthday-date-day\",\n  month: \"birthday-date-month\",\n  year: \"birthday-date-year\",\n}\nconst PART_LABEL: Record<BirthdayDatePart, string> = {\n  day: \"روز\",\n  month: \"ماه\",\n  year: \"سال\",\n}\nconst PART_PLACEHOLDER: Record<BirthdayDatePart, string> = {\n  day: \"روز\",\n  month: \"ماه\",\n  year: \"سال\",\n}\nconst PART_AUTOCOMPLETE: Record<BirthdayDatePart, string> = {\n  day: \"bday-day\",\n  month: \"bday-month\",\n  year: \"bday-year\",\n}\n\nfunction toLatinDigits(value: string) {\n  return value\n    .replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))\n    .replace(/[٠-٩]/g, (digit) => String(ARABIC_DIGITS.indexOf(digit)))\n}\n\nfunction toPersianDigits(value: string) {\n  return toLatinDigits(value).replace(\n    /\\d/g,\n    (digit) => PERSIAN_DIGITS[Number(digit)] ?? digit\n  )\n}\n\nfunction extractDigits(value: string) {\n  return toLatinDigits(value).replace(/\\D/g, \"\")\n}\n\nfunction formatSegmentDigits(digits: string, maxLength: number) {\n  const clipped = extractDigits(digits).slice(0, maxLength)\n  return clipped ? toPersianDigits(clipped) : \"\"\n}\n\nfunction emptyParts(): BirthdayDateParts {\n  return { day: \"\", month: \"\", year: \"\" }\n}\n\nfunction parseBirthdayDateValue(value: string): BirthdayDateParts {\n  const normalized = value.trim()\n  if (!normalized) return emptyParts()\n\n  const separators = normalized.split(/[/\\-.\\s]+/).filter(Boolean)\n  if (separators.length >= 3) {\n    return {\n      day: extractDigits(separators[0] ?? \"\").slice(0, 2),\n      month: extractDigits(separators[1] ?? \"\").slice(0, 2),\n      year: extractDigits(separators[2] ?? \"\").slice(0, 4),\n    }\n  }\n\n  const digits = extractDigits(normalized)\n  if (digits.length <= 2) {\n    return { day: digits, month: \"\", year: \"\" }\n  }\n  if (digits.length <= 4) {\n    return {\n      day: digits.slice(0, 2),\n      month: digits.slice(2),\n      year: \"\",\n    }\n  }\n  return {\n    day: digits.slice(0, 2),\n    month: digits.slice(2, 4),\n    year: digits.slice(4, 8),\n  }\n}\n\nfunction formatBirthdayDateValue(\n  value: string | BirthdayDateParts | null | undefined\n) {\n  const parts =\n    value == null\n      ? emptyParts()\n      : typeof value === \"string\"\n        ? parseBirthdayDateValue(value)\n        : {\n            day: extractDigits(value.day).slice(0, 2),\n            month: extractDigits(value.month).slice(0, 2),\n            year: extractDigits(value.year).slice(0, 4),\n          }\n\n  if (!parts.day && !parts.month && !parts.year) return \"\"\n  return [parts.day, parts.month, parts.year]\n    .map((part) => (part ? toPersianDigits(part) : \"\"))\n    .join(\"/\")\n}\n\nfunction setNativeInputValue(input: HTMLInputElement, next: string) {\n  const descriptor = Object.getOwnPropertyDescriptor(\n    HTMLInputElement.prototype,\n    \"value\"\n  )\n  descriptor?.set?.call(input, next)\n  input.dispatchEvent(new Event(\"input\", { bubbles: true }))\n  input.dispatchEvent(new Event(\"change\", { bubbles: true }))\n}\n\nfunction focusSegment(\n  control: Element | null,\n  part: BirthdayDatePart,\n  options?: { select?: boolean }\n) {\n  const input = control?.querySelector<HTMLInputElement>(\n    `[data-slot=${PART_SLOT[part]}]`\n  )\n  if (!input || input.disabled || input.readOnly) return\n  input.focus()\n  if (options?.select) {\n    input.select()\n  } else {\n    const end = input.value.length\n    input.setSelectionRange(end, end)\n  }\n}\n\nfunction BirthdayDateStoreProvider({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  const [parts, setPartsState] = React.useState<BirthdayDateParts>(emptyParts)\n  const setPart = React.useCallback((part: BirthdayDatePart, digits: string) => {\n    setPartsState((prev) => ({\n      ...prev,\n      [part]: extractDigits(digits).slice(0, MAX_LENGTH[part]),\n    }))\n  }, [])\n  const setParts = React.useCallback((next: BirthdayDateParts) => {\n    setPartsState({\n      day: extractDigits(next.day).slice(0, 2),\n      month: extractDigits(next.month).slice(0, 2),\n      year: extractDigits(next.year).slice(0, 4),\n    })\n  }, [])\n  const store = React.useMemo(\n    () => ({ parts, setPart, setParts }),\n    [parts, setPart, setParts]\n  )\n\n  return (\n    <BirthdayDateStoreContext.Provider value={store}>\n      {children}\n    </BirthdayDateStoreContext.Provider>\n  )\n}\n\nfunction BirthdayDate({\n  className,\n  size = \"default\",\n  name,\n  children,\n  ...props\n}: Omit<React.ComponentProps<typeof FieldPrimitive.Root>, \"size\"> & {\n  size?: BirthdayDateSize\n  name?: string\n}) {\n  return (\n    <BirthdayDateStoreProvider>\n      <BirthdayDateSizeContext.Provider value={size}>\n        <FieldPrimitive.Root\n          data-slot=\"birthday-date\"\n          data-size={size}\n          className={cn(\n            \"group/birthday-date flex w-full min-w-0 flex-col gap-2 data-disabled:cursor-not-allowed data-disabled:opacity-70\",\n            className\n          )}\n          {...props}\n        >\n          {name ? <BirthdayDateHiddenInput name={name} /> : null}\n          {children}\n        </FieldPrimitive.Root>\n      </BirthdayDateSizeContext.Provider>\n    </BirthdayDateStoreProvider>\n  )\n}\n\nfunction BirthdayDateHiddenInput({ name }: { name: string }) {\n  const store = React.useContext(BirthdayDateStoreContext)\n  const value = store\n    ? formatBirthdayDateValue(store.parts)\n    : \"\"\n\n  return <input type=\"hidden\" name={name} value={value} readOnly />\n}\n\nfunction BirthdayDateLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Label>) {\n  return (\n    <FieldPrimitive.Label\n      data-slot=\"birthday-date-label\"\n      className={cn(\n        \"flex w-fit items-center gap-2 text-label leading-none font-normal text-foreground select-none group-data-disabled/birthday-date:cursor-not-allowed group-data-disabled/birthday-date:text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction BirthdayDateControl({\n  className,\n  size: sizeProp,\n  onClick,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  size?: BirthdayDateSize\n}) {\n  const contextSize = React.useContext(BirthdayDateSizeContext)\n  const size = sizeProp ?? contextSize\n\n  return (\n    <div\n      data-slot=\"birthday-date-control\"\n      data-size={size}\n      className={cn(birthdayDateControlVariants({ size }), className)}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        if ((event.target as HTMLElement).closest(\"button,a,input,textarea\")) {\n          return\n        }\n        const day = event.currentTarget.querySelector<HTMLInputElement>(\n          \"[data-slot=birthday-date-day]\"\n        )\n        const target =\n          day && !day.value\n            ? day\n            : event.currentTarget.querySelector<HTMLInputElement>(\n                \"[data-birthday-segment]:not([disabled])\"\n              )\n        if (!target || target.disabled || target.readOnly) return\n        target.focus()\n      }}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nfunction BirthdayDateSeparator({\n  className,\n  children = \"/\",\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"birthday-date-separator\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"shrink-0 select-none text-label font-normal text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  )\n}\n\nfunction BirthdayDateSegment({\n  part,\n  className,\n  size: sizeProp,\n  autoComplete,\n  \"aria-label\": ariaLabel,\n  placeholder,\n  onBeforeInput,\n  onPaste,\n  onInput,\n  onKeyDown,\n  defaultValue,\n  value,\n  ...props\n}: Omit<\n  React.ComponentProps<\"input\">,\n  \"size\" | \"type\" | \"inputMode\" | \"spellCheck\" | \"maxLength\"\n> & {\n  part: BirthdayDatePart\n  size?: BirthdayDateSize\n}) {\n  const contextSize = React.useContext(BirthdayDateSizeContext)\n  const size = sizeProp ?? contextSize\n  const store = React.useContext(BirthdayDateStoreContext)\n  const maxLength = MAX_LENGTH[part]\n  const persianDefault =\n    defaultValue === undefined\n      ? undefined\n      : formatSegmentDigits(String(defaultValue), maxLength)\n  const persianValue =\n    value === undefined\n      ? undefined\n      : formatSegmentDigits(String(value), maxLength)\n\n  const setPart = store?.setPart\n  const setParts = store?.setParts\n\n  React.useLayoutEffect(() => {\n    if (value === undefined) return\n    setPart?.(part, extractDigits(String(persianValue ?? \"\")))\n  }, [value, persianValue, part, setPart])\n\n  React.useLayoutEffect(() => {\n    if (value !== undefined) return\n    setPart?.(part, extractDigits(String(persianDefault ?? \"\")))\n  }, [value, persianDefault, part, setPart])\n\n  const applyValue = (\n    input: HTMLInputElement,\n    rawDigits: string,\n    options?: { advance?: boolean }\n  ) => {\n    const nextDigits = extractDigits(rawDigits).slice(0, maxLength)\n    const next = nextDigits ? toPersianDigits(nextDigits) : \"\"\n    setPart?.(part, nextDigits)\n    if (next !== input.value) {\n      setNativeInputValue(input, next)\n    }\n    if (options?.advance && nextDigits.length >= maxLength) {\n      const index = PART_ORDER.indexOf(part)\n      const nextPart = PART_ORDER[index + 1]\n      if (nextPart) {\n        focusSegment(input.closest(\"[data-slot=birthday-date-control]\"), nextPart, {\n          select: true,\n        })\n      }\n    }\n  }\n\n  return (\n    <input\n      data-slot={PART_SLOT[part]}\n      data-birthday-segment=\"\"\n      data-size={size}\n      data-part={part}\n      aria-label={ariaLabel ?? PART_LABEL[part]}\n      placeholder={placeholder ?? PART_PLACEHOLDER[part]}\n      className={cn(birthdayDateSegmentVariants({ size }), className)}\n      {...props}\n      type=\"text\"\n      inputMode=\"numeric\"\n      size={1}\n      autoComplete={autoComplete ?? PART_AUTOCOMPLETE[part]}\n      spellCheck={false}\n      maxLength={maxLength}\n      defaultValue={persianDefault}\n      value={persianValue}\n      onBeforeInput={(event) => {\n        onBeforeInput?.(event)\n        if (event.defaultPrevented) return\n        if (typeof event.data !== \"string\" || event.data.length === 0) return\n        if (event.data === \"/\" || event.data === \"-\" || event.data === \".\") {\n          event.preventDefault()\n          const index = PART_ORDER.indexOf(part)\n          const nextPart = PART_ORDER[index + 1]\n          if (nextPart) {\n            focusSegment(\n              event.currentTarget.closest(\"[data-slot=birthday-date-control]\"),\n              nextPart,\n              { select: true }\n            )\n          }\n          return\n        }\n        if (!ALLOWED_INSERT.test(event.data)) {\n          event.preventDefault()\n          return\n        }\n        const input = event.currentTarget\n        const start = input.selectionStart ?? input.value.length\n        const end = input.selectionEnd ?? input.value.length\n        const insert = extractDigits(event.data)\n        if (!insert) {\n          event.preventDefault()\n          return\n        }\n        event.preventDefault()\n        const current = extractDigits(input.value)\n        const nextDigits = (\n          current.slice(0, countDigitsBefore(input.value, start)) +\n          insert +\n          current.slice(countDigitsBefore(input.value, end))\n        ).slice(0, maxLength)\n        applyValue(input, nextDigits, { advance: true })\n      }}\n      onPaste={(event) => {\n        onPaste?.(event)\n        if (event.defaultPrevented) return\n        const text = event.clipboardData?.getData(\"text\") ?? \"\"\n        if (!text) return\n        event.preventDefault()\n        const parsed = parseBirthdayDateValue(text)\n        const hasMultiple =\n          Boolean(parsed.day && parsed.month) ||\n          Boolean(parsed.day && parsed.year) ||\n          text.includes(\"/\") ||\n          text.includes(\"-\") ||\n          text.includes(\".\") ||\n          extractDigits(text).length > maxLength\n\n        const input = event.currentTarget\n        const control = input.closest(\"[data-slot=birthday-date-control]\")\n\n        if (hasMultiple && setParts) {\n          setParts(parsed)\n          for (const nextPart of PART_ORDER) {\n            const segment = control?.querySelector<HTMLInputElement>(\n              `[data-slot=${PART_SLOT[nextPart]}]`\n            )\n            if (!segment) continue\n            const next = formatSegmentDigits(parsed[nextPart], MAX_LENGTH[nextPart])\n            if (next !== segment.value) setNativeInputValue(segment, next)\n          }\n          const focusPart =\n            parsed.year.length >= 4\n              ? \"year\"\n              : parsed.month.length >= 2\n                ? \"year\"\n                : parsed.day.length >= 2\n                  ? \"month\"\n                  : \"day\"\n          focusSegment(control, focusPart)\n          return\n        }\n\n        applyValue(input, extractDigits(text), { advance: true })\n      }}\n      onInput={(event) => {\n        onInput?.(event)\n        if (event.defaultPrevented) return\n        const input = event.currentTarget\n        if (value !== undefined) {\n          setPart?.(part, extractDigits(input.value))\n          return\n        }\n        applyValue(input, input.value, { advance: true })\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (event.defaultPrevented) return\n        if (event.key !== \"Backspace\") return\n        const input = event.currentTarget\n        const start = input.selectionStart ?? 0\n        const end = input.selectionEnd ?? 0\n        if (input.value.length > 0 && (start !== 0 || end !== 0)) return\n        if (start !== end) return\n        const index = PART_ORDER.indexOf(part)\n        const prevPart = PART_ORDER[index - 1]\n        if (!prevPart) return\n        event.preventDefault()\n        focusSegment(input.closest(\"[data-slot=birthday-date-control]\"), prevPart)\n      }}\n    />\n  )\n}\n\nfunction countDigitsBefore(value: string, caret: number) {\n  let count = 0\n  const end = Math.max(0, Math.min(caret, value.length))\n  for (let index = 0; index < end; index += 1) {\n    if (DIGIT_CHAR.test(value[index] ?? \"\")) count += 1\n  }\n  return count\n}\n\nfunction BirthdayDateDay(\n  props: Omit<React.ComponentProps<typeof BirthdayDateSegment>, \"part\">\n) {\n  return <BirthdayDateSegment part=\"day\" {...props} />\n}\n\nfunction BirthdayDateMonth(\n  props: Omit<React.ComponentProps<typeof BirthdayDateSegment>, \"part\">\n) {\n  return <BirthdayDateSegment part=\"month\" {...props} />\n}\n\nfunction BirthdayDateYear(\n  props: Omit<React.ComponentProps<typeof BirthdayDateSegment>, \"part\">\n) {\n  return <BirthdayDateSegment part=\"year\" {...props} />\n}\n\nfunction BirthdayDateDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Description>) {\n  return (\n    <FieldPrimitive.Description\n      data-slot=\"birthday-date-description\"\n      className={cn(\n        \"m-0 text-caption font-normal text-muted-foreground group-data-disabled/birthday-date:cursor-not-allowed\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction BirthdayDateError({\n  className,\n  children,\n  match,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Error>) {\n  return (\n    <FieldPrimitive.Validity>\n      {(validity) => {\n        const isInvalid = validity.validity.valid === false\n        if (!isInvalid && match !== true) {\n          return null\n        }\n\n        return (\n          <FieldPrimitive.Error\n            data-slot=\"birthday-date-error\"\n            match={match ?? true}\n            className={cn(\n              \"m-0 text-caption font-normal text-destructive\",\n              className\n            )}\n            {...props}\n          >\n            {children}\n          </FieldPrimitive.Error>\n        )\n      }}\n    </FieldPrimitive.Validity>\n  )\n}\n\nexport {\n  BirthdayDate,\n  BirthdayDateLabel,\n  BirthdayDateControl,\n  BirthdayDateDay,\n  BirthdayDateMonth,\n  BirthdayDateYear,\n  BirthdayDateSeparator,\n  BirthdayDateDescription,\n  BirthdayDateError,\n  formatBirthdayDateValue,\n  parseBirthdayDateValue,\n  birthdayDateControlVariants,\n  birthdayDateSegmentVariants,\n}\nexport type { BirthdayDateParts, BirthdayDatePart }\n"
    }
  ]
}

