{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "input-phone", "title": "Phone Input", "description": "Phone input with country dial code selector", "files": [ { "path": "registry/cs/ui/input-phone/input-phone.tsx", "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { CsBtnColor } from \"@/registry/cs/ui/button/button\"\n\nexport type PhoneCountry = {\n flag: string\n label: string\n dialCode: string\n}\n\nconst DEFAULT_COUNTRIES: PhoneCountry[] = [\n { flag: \"🇨🇿\", label: \"Česká republika\", dialCode: \"+420\" },\n { flag: \"🇸🇰\", label: \"Slovensko\", dialCode: \"+421\" },\n { flag: \"🇵🇱\", label: \"Polsko\", dialCode: \"+48\" },\n { flag: \"🇩🇪\", label: \"Německo\", dialCode: \"+49\" },\n { flag: \"🇦🇹\", label: \"Rakousko\", dialCode: \"+43\" },\n { flag: \"🇫🇷\", label: \"Francie\", dialCode: \"+33\" },\n { flag: \"🇮🇹\", label: \"Itálie\", dialCode: \"+39\" },\n { flag: \"🇪🇸\", label: \"Španělsko\", dialCode: \"+34\" },\n { flag: \"🇨🇭\", label: \"Švýcarsko\", dialCode: \"+41\" },\n { flag: \"🇬🇧\", label: \"Velká Británie\", dialCode: \"+44\" },\n { flag: \"🇭🇷\", label: \"Chorvatsko\", dialCode: \"+385\" },\n { flag: \"🇭🇺\", label: \"Maďarsko\", dialCode: \"+36\" },\n { flag: \"🇺🇦\", label: \"Ukrajina\", dialCode: \"+380\" },\n { flag: \"🇺🇸\", label: \"USA\", dialCode: \"+1\" },\n { flag: \"🇨🇦\", label: \"Kanada\", dialCode: \"+1\" },\n { flag: \"🇦🇺\", label: \"Austrálie\", dialCode: \"+61\" },\n { flag: \"🇯🇵\", label: \"Japonsko\", dialCode: \"+81\" },\n { flag: \"🇨🇳\", label: \"Čína\", dialCode: \"+86\" },\n { flag: \"🇮🇳\", label: \"Indie\", dialCode: \"+91\" },\n { flag: \"🇧🇷\", label: \"Brazílie\", dialCode: \"+55\" },\n { flag: \"🇷🇺\", label: \"Rusko\", dialCode: \"+7\" },\n { flag: \"🇮🇱\", label: \"Izrael\", dialCode: \"+972\" },\n { flag: \"🇰🇷\", label: \"Jižní Korea\", dialCode: \"+82\" },\n]\n\nfunction normalizePhoneLocal(phone: string) {\n return phone.replace(/\\D/g, \"\")\n}\n\nfunction buildFullPhone(dialCode: string, phone: string) {\n return `${dialCode}${normalizePhoneLocal(phone)}`\n}\n\ntype PhoneRule = {\n groups: number[]\n maxDigits: number\n}\n\nconst PHONE_RULES: Record = {\n \"+420\": { groups: [3, 3, 3], maxDigits: 9 },\n \"+421\": { groups: [3, 3, 3], maxDigits: 9 },\n \"+48\": { groups: [3, 3, 3], maxDigits: 9 },\n \"+49\": { groups: [3, 4, 4], maxDigits: 11 },\n \"+43\": { groups: [3, 3, 4], maxDigits: 10 },\n \"+33\": { groups: [1, 2, 2, 2, 2], maxDigits: 9 },\n \"+39\": { groups: [3, 3, 4], maxDigits: 10 },\n \"+34\": { groups: [3, 3, 3], maxDigits: 9 },\n \"+41\": { groups: [2, 3, 2, 2], maxDigits: 9 },\n \"+44\": { groups: [4, 3, 3], maxDigits: 10 },\n \"+385\": { groups: [2, 3, 4], maxDigits: 9 },\n \"+36\": { groups: [2, 3, 4], maxDigits: 9 },\n \"+380\": { groups: [2, 3, 2, 2], maxDigits: 9 },\n \"+1\": { groups: [3, 3, 4], maxDigits: 10 },\n \"+61\": { groups: [3, 3, 3], maxDigits: 9 },\n \"+81\": { groups: [2, 4, 4], maxDigits: 10 },\n \"+86\": { groups: [3, 4, 4], maxDigits: 11 },\n \"+91\": { groups: [5, 5], maxDigits: 10 },\n \"+55\": { groups: [2, 5, 4], maxDigits: 11 },\n \"+7\": { groups: [3, 3, 2, 2], maxDigits: 10 },\n \"+972\": { groups: [2, 3, 4], maxDigits: 9 },\n \"+82\": { groups: [2, 4, 4], maxDigits: 10 },\n}\n\nfunction getPhoneRule(dialCode: string): PhoneRule {\n return PHONE_RULES[dialCode] ?? { groups: [3, 3, 4], maxDigits: 10 }\n}\n\nfunction formatPhoneByDialCode(\n dialCode: string,\n value: string,\n maxDigitsOverride?: number\n) {\n const rule = getPhoneRule(dialCode)\n const maxDigits = maxDigitsOverride ?? rule.maxDigits\n const digits = normalizePhoneLocal(value).slice(0, maxDigits)\n\n const parts: string[] = []\n let cursor = 0\n for (const size of rule.groups) {\n if (cursor >= digits.length) break\n parts.push(digits.slice(cursor, cursor + size))\n cursor += size\n }\n\n if (cursor < digits.length) {\n parts.push(digits.slice(cursor))\n }\n\n return parts.join(\" \")\n}\n\nconst PHONE_SIZES = {\n xs: {\n root: \"h-8 text-xs\",\n select: \"px-2\",\n input: \"px-2\",\n },\n sm: {\n root: \"h-9 text-sm\",\n select: \"px-2.5\",\n input: \"px-2.5\",\n },\n md: {\n root: \"h-10 text-sm\",\n select: \"px-3\",\n input: \"px-3\",\n },\n lg: {\n root: \"h-11 text-base\",\n select: \"px-3\",\n input: \"px-3\",\n },\n xl: {\n root: \"h-12 text-base\",\n select: \"px-4\",\n input: \"px-4\",\n },\n}\n\nconst PHONE_RADII = {\n xs: \"rounded-none\",\n sm: \"rounded-sm\",\n md: \"rounded-md\",\n lg: \"rounded-lg\",\n xl: \"rounded-full\",\n}\n\nconst PHONE_SELECT_COLORS: Record = {\n primary: \"text-cs-primary\",\n secondary: \"text-cs-secondary\",\n grey: \"text-cs-grey\",\n dark: \"text-cs-dark\",\n red: \"text-cs-red\",\n blue: \"text-cs-blue\",\n green: \"text-cs-green\",\n yellow: \"text-cs-yellow\",\n orange: \"text-cs-orange\",\n}\n\nconst PHONE_FRAME_COLORS: Record = {\n primary: \"border-cs-primary/40 focus-within:border-cs-primary/60 focus-within:ring-cs-primary/20\",\n secondary: \"border-cs-secondary/40 focus-within:border-cs-secondary/60 focus-within:ring-cs-secondary/20\",\n grey: \"border-cs-grey/40 focus-within:border-cs-grey/60 focus-within:ring-cs-grey/20\",\n dark: \"border-cs-dark/40 focus-within:border-cs-dark/60 focus-within:ring-cs-dark/20\",\n red: \"border-cs-red/40 focus-within:border-cs-red/60 focus-within:ring-cs-red/20\",\n blue: \"border-cs-blue/40 focus-within:border-cs-blue/60 focus-within:ring-cs-blue/20\",\n green: \"border-cs-green/40 focus-within:border-cs-green/60 focus-within:ring-cs-green/20\",\n yellow: \"border-cs-yellow/50 focus-within:border-cs-yellow/70 focus-within:ring-cs-yellow/20\",\n orange: \"border-cs-orange/40 focus-within:border-cs-orange/60 focus-within:ring-cs-orange/20\",\n}\n\nconst PHONE_BACKGROUNDS: Record = {\n primary: \"bg-cs-primary/10\",\n secondary: \"bg-cs-secondary/10\",\n grey: \"bg-cs-grey/10\",\n dark: \"bg-cs-dark/10\",\n red: \"bg-cs-red/10\",\n blue: \"bg-cs-blue/10\",\n green: \"bg-cs-green/10\",\n yellow: \"bg-cs-yellow/15\",\n orange: \"bg-cs-orange/10\",\n white: \"bg-white\",\n light: \"bg-cs-grey-light\",\n}\n\nexport interface PhoneInputProps\n extends Omit, \"type\" | \"onChange\" | \"size\"> {\n countries?: PhoneCountry[]\n defaultDialCode?: string\n defaultPhone?: string\n dialCodeName?: string\n fullPhoneName?: string\n onPhoneChange?: (value: string) => void\n onDialCodeChange?: (dialCode: string) => void\n onFullPhoneChange?: (\n value: string,\n detail: { dialCode: string; phone: string }\n ) => void\n label?: string\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n radius?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n color?: CsBtnColor\n backgroundColor?: CsBtnColor | \"white\" | \"light\"\n phoneMaxLength?: number\n}\n\nconst PhoneInput = React.forwardRef(\n (\n {\n className,\n countries = DEFAULT_COUNTRIES,\n defaultDialCode = \"+420\",\n defaultPhone = \"\",\n dialCodeName = \"dialCode\",\n fullPhoneName = \"phone\",\n onPhoneChange,\n onDialCodeChange,\n onFullPhoneChange,\n label,\n name,\n id: idProp,\n placeholder = \"123 456 789\",\n size = \"md\",\n radius = \"xl\",\n color = \"blue\",\n backgroundColor = \"blue\",\n phoneMaxLength,\n value,\n defaultValue,\n ...props\n },\n ref\n ) => {\n const autoId = React.useId()\n const id = idProp ?? autoId\n\n const initialCode =\n countries.find((country) => country.dialCode === defaultDialCode)?.dialCode ??\n countries[0]?.dialCode ??\n \"+420\"\n\n const [dialCode, setDialCode] = React.useState(initialCode)\n const [phone, setPhone] = React.useState(() =>\n formatPhoneByDialCode(\n initialCode,\n typeof defaultValue === \"string\" ? defaultValue : defaultPhone,\n phoneMaxLength\n )\n )\n\n const isControlled = typeof value === \"string\"\n const phoneValue = isControlled\n ? formatPhoneByDialCode(dialCode, value, phoneMaxLength)\n : phone\n const fullPhone = buildFullPhone(dialCode, phoneValue)\n const maxDigitsForDial = phoneMaxLength ?? getPhoneRule(dialCode).maxDigits\n const maxFormattedLength = formatPhoneByDialCode(\n dialCode,\n \"9\".repeat(maxDigitsForDial),\n maxDigitsForDial\n ).length\n\n React.useEffect(() => {\n onFullPhoneChange?.(fullPhone, { dialCode, phone: phoneValue })\n }, [dialCode, fullPhone, onFullPhoneChange, phoneValue])\n\n const handlePhoneChange = (event: React.ChangeEvent) => {\n const formattedValue = formatPhoneByDialCode(\n dialCode,\n event.target.value,\n phoneMaxLength\n )\n\n if (!isControlled) {\n setPhone(formattedValue)\n }\n onPhoneChange?.(formattedValue)\n }\n\n const handleDialCodeChange = (event: React.ChangeEvent) => {\n const nextDialCode = event.target.value\n setDialCode(nextDialCode)\n\n if (!isControlled) {\n setPhone((current) =>\n formatPhoneByDialCode(nextDialCode, current, phoneMaxLength)\n )\n }\n\n onDialCodeChange?.(nextDialCode)\n }\n\n return (\n
\n {label ? (\n \n ) : null}\n\n \n
\n \n {countries.map((country) => (\n \n ))}\n \n \n
\n\n \n\n
\n\n {fullPhoneName ? (\n \n ) : null}\n \n )\n }\n)\n\nPhoneInput.displayName = \"PhoneInput\"\n\nexport { PhoneInput, buildFullPhone, normalizePhoneLocal }\n", "type": "registry:ui" } ], "type": "registry:ui" }