{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CurvedInput-TS-TW",
	"title": "CurvedInput",
	"description": "Arc-bent input bar with text, caret and submit button all following the curve.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "CurvedInput/CurvedInput.tsx",
			"content": "import {\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n  type ChangeEvent,\n  type FormEvent,\n  type KeyboardEvent,\n  type MouseEvent as ReactMouseEvent,\n  type PointerEvent as ReactPointerEvent,\n  type SyntheticEvent\n} from 'react';\n\nconst DEG = 180 / Math.PI;\n\nconst round2 = (n: number): number => Math.round(n * 100) / 100;\n\nconst hexToRgba = (hex: string, alpha: number): string => {\n  let h = String(hex).replace('#', '');\n  if (h.length === 3)\n    h = h\n      .split('')\n      .map(c => c + c)\n      .join('');\n  const n = parseInt(h.slice(0, 6), 16);\n  if (Number.isNaN(n)) return hex;\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n};\n\ntype ShadowSize = 'sm' | 'md' | 'lg';\ntype Theme = 'dark' | 'light';\n\nconst SHADOWS: Record<ShadowSize, [number, number, number]> = {\n  sm: [5, 12, 0.3],\n  md: [10, 24, 0.4],\n  lg: [16, 40, 0.52]\n};\n\ninterface ThemePalette {\n  backgroundColor: string;\n  textColor: string;\n  placeholderColor: string;\n  borderColor: string;\n  buttonColor: string;\n  buttonTextColor: string;\n  shadowColor: string;\n}\n\nconst THEMES: Record<Theme, ThemePalette> = {\n  dark: {\n    backgroundColor: '#1B1722',\n    textColor: '#f5f5f5',\n    placeholderColor: '#a1a1aa',\n    borderColor: '#392e4e',\n    buttonColor: '#A855F7',\n    buttonTextColor: '#ffffff',\n    shadowColor: '#000000'\n  },\n  light: {\n    backgroundColor: '#ffffff',\n    textColor: '#1d2050',\n    placeholderColor: '#9aa0b6',\n    borderColor: '#262a56',\n    buttonColor: '#4763eb',\n    buttonTextColor: '#ffffff',\n    shadowColor: '#0b0e2a'\n  }\n};\n\ninterface Geometry {\n  straight: boolean;\n  W: number;\n  T: number;\n  svgH: number;\n  R?: number;\n  dir?: number;\n  uPerLen: number;\n  point: (u: number, v: number) => [number, number];\n  angleAt: (u: number) => number;\n  uFromPoint: (x: number, y?: number) => number;\n}\n\n// Maps the flat coordinate space (u: 0..W along the bar, v: offset from the\n// centerline, positive down) onto a circular arc with the given sagitta\n// (`bend`, in px). Positive bend arches up, negative sags down, 0 is flat.\nconst buildGeometry = (width: number, bend: number, thickness: number, pad: number): Geometry => {\n  const W = width;\n  const T = thickness;\n  const s = Math.max(-W * 0.35, Math.min(bend, W * 0.35));\n  const a = Math.abs(s);\n  const dir = s >= 0 ? 1 : -1;\n  const svgH = T + a + pad * 2;\n\n  if (a < 0.75) {\n    const midY = pad + T / 2;\n    return {\n      straight: true,\n      W,\n      T,\n      svgH,\n      uPerLen: 1,\n      point: (u, v) => [u, midY + v],\n      angleAt: () => 0,\n      uFromPoint: x => x\n    };\n  }\n\n  const R = (W * W * 0.25 + a * a) / (2 * a);\n  const cx = W / 2;\n  const apexY = pad + T / 2 + (dir > 0 ? 0 : a);\n  const cy = apexY + dir * R;\n  const phi = Math.asin(Math.min(1, W / (2 * R)));\n\n  return {\n    straight: false,\n    W,\n    T,\n    svgH,\n    R,\n    dir,\n    uPerLen: W / (2 * R * phi),\n    point: (u, v) => {\n      const th = ((u - cx) / cx) * phi;\n      const rho = R - dir * v;\n      return [cx + rho * Math.sin(th), cy - dir * rho * Math.cos(th)];\n    },\n    angleAt: u => dir * ((u - cx) / cx) * phi * DEG,\n    uFromPoint: (x, y = 0) => {\n      const th = Math.atan2(x - cx, dir * (cy - y));\n      return cx + (th / phi) * cx;\n    }\n  };\n};\n\nconst fmt = (g: Geometry, u: number, v: number): string => {\n  const [x, y] = g.point(u, v);\n  return `${round2(x)} ${round2(y)}`;\n};\n\n// Segment along a constant-v edge, as a circular arc (or a line when flat)\nconst edgeSeg = (g: Geometry, uTo: number, v: number, ltr: boolean): string => {\n  if (g.straight) return `L ${fmt(g, uTo, v)}`;\n  const rho = round2(g.R! - g.dir! * v);\n  const sweep = ltr === g.dir! > 0 ? 1 : 0;\n  return `A ${rho} ${rho} 0 0 ${sweep} ${fmt(g, uTo, v)}`;\n};\n\n// A rectangle bent along the arc: circular top/bottom edges, radial end caps\n// and quadratic rounded corners.\nconst bentRectPath = (g: Geometry, u0: number, u1: number, vTop: number, vBot: number, radius: number): string => {\n  const rc = Math.max(0, Math.min(radius, (vBot - vTop) / 2, (u1 - u0) / 2));\n  return [\n    `M ${fmt(g, u0 + rc, vTop)}`,\n    edgeSeg(g, u1 - rc, vTop, true),\n    `Q ${fmt(g, u1, vTop)} ${fmt(g, u1, vTop + rc)}`,\n    `L ${fmt(g, u1, vBot - rc)}`,\n    `Q ${fmt(g, u1, vBot)} ${fmt(g, u1 - rc, vBot)}`,\n    edgeSeg(g, u0 + rc, vBot, false),\n    `Q ${fmt(g, u0, vBot)} ${fmt(g, u0, vBot - rc)}`,\n    `L ${fmt(g, u0, vTop + rc)}`,\n    `Q ${fmt(g, u0, vTop)} ${fmt(g, u0 + rc, vTop)}`,\n    'Z'\n  ].join(' ');\n};\n\nconst bentLinePath = (g: Geometry, u0: number, u1: number, v: number): string =>\n  `M ${fmt(g, u0, v)} ${edgeSeg(g, u1, v, true)}`;\n\nconst SELECTABLE_TYPES = ['text', 'search', 'tel', 'url', 'password'];\n\ninterface CurvedInputProps {\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  onSubmit?: (value: string) => void;\n  placeholder?: string;\n  buttonText?: string;\n  type?: string;\n  name?: string;\n  ariaLabel?: string;\n  theme?: Theme;\n  width?: number | string;\n  bend?: number;\n  height?: number;\n  cornerRadius?: number;\n  borderWidth?: number;\n  fontSize?: number;\n  backgroundColor?: string;\n  textColor?: string;\n  placeholderColor?: string;\n  borderColor?: string;\n  buttonColor?: string;\n  buttonTextColor?: string;\n  iconColor?: string;\n  shadowSize?: ShadowSize;\n  shadowColor?: string;\n  showButton?: boolean;\n  showIcon?: boolean;\n  icon?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n}\n\nconst CurvedInput = ({\n  value,\n  defaultValue = '',\n  onChange,\n  onSubmit,\n  placeholder = 'Enter your email',\n  buttonText = 'Get Started',\n  type = 'email',\n  name,\n  ariaLabel,\n  theme = 'dark',\n  width = 450,\n  bend = 28,\n  height = 64,\n  cornerRadius = 18,\n  borderWidth = 1.5,\n  fontSize = 16,\n  backgroundColor,\n  textColor,\n  placeholderColor,\n  borderColor,\n  buttonColor,\n  buttonTextColor,\n  iconColor,\n  shadowSize = 'md',\n  shadowColor,\n  showButton = true,\n  showIcon = true,\n  icon,\n  className = '',\n  style\n}: CurvedInputProps) => {\n  const uid = useId().replace(/:/g, '');\n  const layoutPathId = `ci-text-${uid}`;\n  const buttonPathId = `ci-btn-${uid}`;\n  const clipId = `ci-clip-${uid}`;\n\n  const rootRef = useRef<HTMLFormElement | null>(null);\n  const svgRef = useRef<SVGSVGElement | null>(null);\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const textRef = useRef<SVGTextElement | null>(null);\n  const btnMeasureRef = useRef<SVGTextElement | null>(null);\n  const scrollRef = useRef(0);\n\n  const [w, setW] = useState(0);\n  const [innerValue, setInnerValue] = useState(defaultValue);\n  const [caretIndex, setCaretIndex] = useState(defaultValue.length);\n  const [focused, setFocused] = useState(false);\n  const [caretU, setCaretU] = useState(0);\n  const [scrollLen, setScrollLen] = useState(0);\n  const [btnTextW, setBtnTextW] = useState(0);\n  const [, setFontTick] = useState(0);\n\n  const val = value !== undefined ? value : innerValue;\n  const display = type === 'password' ? '•'.repeat(val.length) : val;\n\n  const palette = THEMES[theme] || THEMES.dark;\n  const bgColor = backgroundColor ?? palette.backgroundColor;\n  const fgColor = textColor ?? palette.textColor;\n  const phColor = placeholderColor ?? palette.placeholderColor;\n  const strokeColor = borderColor ?? palette.borderColor;\n  const accentColor = buttonColor ?? palette.buttonColor;\n  const btnFgColor = buttonTextColor ?? palette.buttonTextColor;\n  const shColor = shadowColor ?? palette.shadowColor;\n\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(entries => {\n      const cw = entries[0]?.contentRect?.width ?? el.clientWidth;\n      setW(Math.round(cw));\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  // Re-measure once webfonts finish loading\n  useEffect(() => {\n    let alive = true;\n    if (document.fonts?.ready) {\n      document.fonts.ready.then(() => {\n        if (alive) setFontTick(t => t + 1);\n      });\n    }\n    return () => {\n      alive = false;\n    };\n  }, []);\n\n  const pad = Math.ceil(borderWidth / 2) + 6;\n  const geom = useMemo<Geometry | null>(\n    () => (w > 2 ? buildGeometry(w, bend, height, pad) : null),\n    [w, bend, height, pad]\n  );\n\n  const layout = useMemo(() => {\n    if (!geom) return null;\n    const T = height;\n    const btnInset = Math.max(5, borderWidth + 4);\n    const chipH = Math.min(34, Math.max(16, T * 0.34));\n    const chipW = chipH * 1.25;\n    const iconU = 22 + chipW / 2;\n    const textStartU = showIcon ? 22 + chipW + 13 : 24;\n    const btnW = showButton ? Math.max(btnTextW + fontSize * 2.7, T * 1.35) : 0;\n    const btnU1 = geom.W - btnInset;\n    const btnU0 = btnU1 - btnW;\n    const textEndU = Math.max(textStartU + 20, showButton ? btnU0 - 14 : geom.W - 24);\n    const winLen = (textEndU - textStartU) / geom.uPerLen;\n    return { btnInset, chipH, chipW, iconU, textStartU, textEndU, btnU0, btnU1, winLen };\n  }, [geom, height, borderWidth, btnTextW, fontSize, showIcon, showButton]);\n\n  // Measure rendered text to keep the caret on the curve and scroll long\n  // values along the arc, exactly like a native input would.\n  useLayoutEffect(() => {\n    if (btnMeasureRef.current) {\n      const bw = btnMeasureRef.current.getComputedTextLength();\n      setBtnTextW(prev => (Math.abs(prev - bw) > 0.5 ? bw : prev));\n    }\n    if (!geom || !layout) return;\n    const textEl = textRef.current;\n    const caret = Math.min(caretIndex, display.length);\n    let caretLen = 0;\n    let totalLen = 0;\n    if (textEl && display.length) {\n      try {\n        totalLen = textEl.getSubStringLength(0, display.length);\n        caretLen = caret > 0 ? textEl.getSubStringLength(0, caret) : 0;\n      } catch {\n        totalLen = 0;\n        caretLen = 0;\n      }\n    }\n    let next = scrollRef.current;\n    if (caretLen - next > layout.winLen - 2) next = caretLen - layout.winLen + 2;\n    if (caretLen - next < 0) next = caretLen;\n    if (totalLen - next < layout.winLen) next = Math.max(0, totalLen - layout.winLen);\n    next = Math.max(0, next);\n    if (Math.abs(next - scrollRef.current) > 0.5) {\n      scrollRef.current = next;\n      setScrollLen(next);\n    }\n    setCaretU(layout.textStartU + (caretLen - next) * geom.uPerLen);\n  });\n\n  const commitValue = (v: string) => {\n    if (value === undefined) setInnerValue(v);\n    onChange?.(v);\n  };\n\n  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    commitValue(e.target.value);\n    setCaretIndex(e.target.selectionStart ?? e.target.value.length);\n  };\n\n  const handleSelect = (e: SyntheticEvent<HTMLInputElement>) => {\n    const target = e.currentTarget;\n    setCaretIndex(target.selectionStart ?? target.value.length);\n  };\n\n  const handleSubmit = (e?: FormEvent<HTMLFormElement>) => {\n    if (e?.preventDefault) e.preventDefault();\n    if (onSubmit) onSubmit(val);\n  };\n\n  // Click on the curve: focus the hidden input and drop the caret on the\n  // character closest to the click, measured in arc length.\n  const handleSurfaceClick = (e: ReactMouseEvent<SVGSVGElement>) => {\n    const input = inputRef.current;\n    if (!input) return;\n    let idx = display.length;\n    const svg = svgRef.current;\n    const textEl = textRef.current;\n    if (svg && geom && layout && textEl && display.length) {\n      try {\n        const ctm = svg.getScreenCTM();\n        if (!ctm) throw new Error('missing screen CTM');\n        const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse());\n        const target = scrollRef.current + (geom.uFromPoint(pt.x, pt.y) - layout.textStartU) / geom.uPerLen;\n        let best = 0;\n        let bestDist = Infinity;\n        for (let i = 0; i <= display.length; i++) {\n          const li = i === 0 ? 0 : textEl.getSubStringLength(0, i);\n          const d = Math.abs(li - target);\n          if (d < bestDist) {\n            bestDist = d;\n            best = i;\n          }\n        }\n        idx = best;\n      } catch {\n        idx = display.length;\n      }\n    }\n    input.focus();\n    try {\n      input.setSelectionRange(idx, idx);\n    } catch {\n      /* selection API unavailable for this input type */\n    }\n    setCaretIndex(idx);\n  };\n\n  const safeType = SELECTABLE_TYPES.includes(type) ? type : 'text';\n  const inputMode = type === 'email' ? 'email' : type === 'number' ? 'decimal' : undefined;\n\n  const shadow = SHADOWS[shadowSize];\n  const svgStyle: CSSProperties | undefined = shadow\n    ? { filter: `drop-shadow(0 ${shadow[0]}px ${shadow[1]}px ${hexToRgba(shColor, shadow[2])})` }\n    : undefined;\n\n  let content: ReactNode = null;\n  if (geom && layout) {\n    const T = height;\n    const vBase = fontSize * 0.34;\n    const scrollU = scrollLen * geom.uPerLen;\n    const bandPath = bentRectPath(geom, 0, geom.W, -T / 2, T / 2, cornerRadius);\n    const layoutPath = bentLinePath(geom, layout.textStartU - scrollU, geom.W, vBase);\n    const clipPath = bentRectPath(geom, layout.textStartU - 6, layout.textEndU + 8, -T / 2, T / 2, 0);\n\n    const chipFill = iconColor || accentColor;\n    const { chipW, chipH } = layout;\n    const ew = chipW * 0.5;\n    const eh = chipH * 0.5;\n    const sw = Math.max(1.1, chipH * 0.075);\n    const [ix, iy] = geom.point(layout.iconU, 0);\n    const iconAngle = geom.angleAt(layout.iconU);\n\n    const [caretX, caretY] = geom.point(caretU, 0);\n    const caretAngle = geom.angleAt(caretU);\n    const caretH = Math.min(T * 0.58, fontSize * 1.45);\n\n    const btnH = T - layout.btnInset * 2;\n    const buttonPath = showButton\n      ? bentRectPath(\n          geom,\n          layout.btnU0,\n          layout.btnU1,\n          -T / 2 + layout.btnInset,\n          T / 2 - layout.btnInset,\n          Math.min(cornerRadius * 0.72, btnH / 2)\n        )\n      : '';\n    const buttonTextPath = showButton ? bentLinePath(geom, layout.btnU0, layout.btnU1, vBase) : '';\n\n    content = (\n      <svg\n        ref={svgRef}\n        className=\"block w-full h-auto overflow-visible [&_text]:font-[inherit] cursor-text select-none [-webkit-tap-highlight-color:transparent]\"\n        width={geom.W}\n        height={round2(geom.svgH)}\n        viewBox={`0 0 ${geom.W} ${round2(geom.svgH)}`}\n        style={svgStyle}\n        onPointerDown={e => e.preventDefault()}\n        onClick={handleSurfaceClick}\n      >\n        <defs>\n          <clipPath id={clipId}>\n            <path d={clipPath} />\n          </clipPath>\n        </defs>\n\n        <path\n          className={`opacity-0 transition-opacity duration-[250ms] ease-in-out ${focused ? 'opacity-[0.28]' : ''}`}\n          d={bandPath}\n          fill=\"none\"\n          stroke={accentColor}\n          strokeWidth={borderWidth + 6}\n        />\n        <path d={bandPath} fill={bgColor} stroke={strokeColor} strokeWidth={borderWidth} />\n\n        <path id={layoutPathId} d={layoutPath} fill=\"none\" />\n\n        {showIcon && (\n          <g transform={`translate(${round2(ix)} ${round2(iy)}) rotate(${round2(iconAngle)})`} aria-hidden=\"true\">\n            {icon || (\n              <>\n                <rect x={-chipW / 2} y={-chipH / 2} width={chipW} height={chipH} rx={chipH * 0.27} fill={chipFill} />\n                <rect\n                  x={-ew / 2}\n                  y={-eh / 2}\n                  width={ew}\n                  height={eh}\n                  rx={1.4}\n                  fill=\"none\"\n                  stroke=\"#ffffff\"\n                  strokeWidth={sw}\n                  strokeLinejoin=\"round\"\n                />\n                <path\n                  d={`M ${round2(-ew / 2)} ${round2(-eh / 2 + sw * 0.4)} L 0 ${round2(eh * 0.14)} L ${round2(ew / 2)} ${round2(-eh / 2 + sw * 0.4)}`}\n                  fill=\"none\"\n                  stroke=\"#ffffff\"\n                  strokeWidth={sw}\n                  strokeLinejoin=\"round\"\n                  strokeLinecap=\"round\"\n                />\n              </>\n            )}\n          </g>\n        )}\n\n        <g clipPath={`url(#${clipId})`}>\n          <text\n            ref={textRef}\n            style={{ fontSize: `${fontSize}px`, fontWeight: 500 }}\n            fill={fgColor}\n            xmlSpace=\"preserve\"\n            aria-hidden=\"true\"\n          >\n            <textPath href={`#${layoutPathId}`}>{display}</textPath>\n          </text>\n          {!display && placeholder && (\n            <text\n              style={{ fontSize: `${fontSize}px`, fontWeight: 500 }}\n              fill={phColor}\n              xmlSpace=\"preserve\"\n              aria-hidden=\"true\"\n            >\n              <textPath href={`#${layoutPathId}`}>{placeholder}</textPath>\n            </text>\n          )}\n          {focused && (\n            <g\n              key={`${display}-${Math.min(caretIndex, display.length)}`}\n              transform={`translate(${round2(caretX)} ${round2(caretY)}) rotate(${round2(caretAngle)})`}\n            >\n              <line y1={-caretH / 2} y2={caretH / 2} stroke={fgColor} strokeWidth=\"1.5\" strokeLinecap=\"round\">\n                <animate\n                  attributeName=\"opacity\"\n                  values=\"1;0\"\n                  dur=\"1.06s\"\n                  calcMode=\"discrete\"\n                  repeatCount=\"indefinite\"\n                />\n              </line>\n            </g>\n          )}\n        </g>\n\n        {showButton && (\n          <g\n            className=\"group outline-none cursor-pointer\"\n            role=\"button\"\n            tabIndex={0}\n            aria-label={buttonText}\n            onClick={e => {\n              e.stopPropagation();\n              handleSubmit();\n            }}\n            onPointerDown={(e: ReactPointerEvent<SVGGElement>) => e.stopPropagation()}\n            onKeyDown={(e: KeyboardEvent<SVGGElement>) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                handleSubmit();\n              }\n            }}\n          >\n            <path\n              className=\"group-active:brightness-[0.94] group-focus-visible:brightness-[1.18] group-hover:brightness-[1.12] transition-[filter,opacity] duration-200 ease-in-out\"\n              d={buttonPath}\n              fill={accentColor}\n            />\n            <path id={buttonPathId} d={buttonTextPath} fill=\"none\" />\n            <text\n              fill={btnFgColor}\n              textAnchor=\"middle\"\n              style={{ fontSize: `${fontSize}px`, fontWeight: 600, pointerEvents: 'none' }}\n            >\n              <textPath href={`#${buttonPathId}`} startOffset=\"50%\">\n                {buttonText}\n              </textPath>\n            </text>\n          </g>\n        )}\n\n        <text\n          ref={btnMeasureRef}\n          style={{ fontSize: `${fontSize}px`, fontWeight: 600 }}\n          x=\"-9999\"\n          y=\"-9999\"\n          visibility=\"hidden\"\n          aria-hidden=\"true\"\n        >\n          {buttonText}\n        </text>\n      </svg>\n    );\n  }\n\n  return (\n    <form\n      ref={rootRef}\n      className={`relative block w-full max-w-full m-0 ${className}`.trim()}\n      style={{ width: typeof width === 'number' ? `${width}px` : width, ...style }}\n      onSubmit={handleSubmit}\n      noValidate\n    >\n      {content}\n      <input\n        ref={inputRef}\n        className=\"absolute inset-0 bg-transparent opacity-0 m-0 p-0 border-0 outline-none w-full h-full text-transparent text-base pointer-events-none [caret-color:transparent]\"\n        type={safeType}\n        inputMode={inputMode}\n        name={name}\n        value={val}\n        onChange={handleInputChange}\n        onSelect={handleSelect}\n        onKeyUp={handleSelect}\n        onFocus={() => setFocused(true)}\n        onBlur={() => setFocused(false)}\n        aria-label={ariaLabel || placeholder || 'Curved input'}\n        autoComplete=\"off\"\n        autoCapitalize=\"none\"\n        autoCorrect=\"off\"\n        spellCheck={false}\n      />\n    </form>\n  );\n};\n\nexport default CurvedInput;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}