diff --git a/README.md b/README.md
index 305ea33..9b6169e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,76 @@
-# react-basic-contenteditable
-React 18 contenteditable component. Super-customizable!
+# React Basic Contenteditable
+
+![React Basic Content Editable](readme-header-img.png)
+
+A React component that allows you to create an editable area in your application. It's perfect for situations where you need to provide a user-friendly, in-place editing functionality.
+
+## Installation
+
+Install via npm
+
+```sh
+npm install --save react-basic-contenteditable
+```
+
+or yarn
+
+```sh
+yarn add react-basic-contenteditable
+```
+
+## Usage
+
+
+
+### Example
+
+```javascript
+import ContentEditable from "react-basic-contenteditable"
+
+const App = () => {
+ const [content, setContent] = useState("")
+
+ return (
+
+
{content}
+ setContent(content)}
+ />
+
+ )
+}
+
+export default App
+```
+
+### Props
+
+> All props except `onChange` are optional.
+
+| Name | Optional | Type | Description |
+| ------------------------ | -------- | ------------------- | --------------------------------------------------------------------- |
+| containerClassName | ✔️ | `string` | Custom classes for the wrapper div |
+| contentEditableClassName | ✔️ | `string` | Custom classes for the input element |
+| placeholderClassName | ✔️ | `string` | Custom classes for the placeholder text |
+| placeholder | ✔️ | `string` | Input placeholder text |
+| disabled | ✔️ | `boolean` | Flag that disables the input element |
+| updatedContent | ✔️ | `string` | Text injected from parent element into the input as the current value |
+| onContentExternalUpdate | ✔️ | `(content) => void` | Method that emits the injected content by the `updatedContent` prop |
+| onChange | ❌ | `(content) => void` | Method that emits the current content as a string |
+| onKeyUp | ✔️ | `(e) => void` | Method that emits the keyUp keyboard event |
+| onKeyDown | ✔️ | `(e) => void` | Method that emits the keyDown keyboard event |
+| onFocus | ✔️ | `(e) => void` | Method that emits the focus event |
+| onBlur | ✔️ | `(e) => void` | Method that emits the blur event |
+
+## Contribution
+
+If you have a suggestion that would make this component better feel free to fork the project and open a pull request or create an issue for any idea or bug you find.\
+Remeber to follow the [Contributing Guidelines](https://github.com/ChrisUser/.github/blob/main/CONTRIBUTING.md).
+
+## Licence
+
+React Basic Contenteditable is [MIT licensed](https://github.com/ChrisUser/react-basic-contenteditable/blob/master/LICENSE).
diff --git a/index.html b/index.html
index e4b78ea..9782ab5 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,10 @@
-
+
- Vite + React + TS
+ react-basic-contenteditable
diff --git a/lib/ContentEditable.tsx b/lib/ContentEditable.tsx
new file mode 100644
index 0000000..73c6c3b
--- /dev/null
+++ b/lib/ContentEditable.tsx
@@ -0,0 +1,330 @@
+import React, { useState, useEffect, useRef, useCallback } from "react"
+
+interface ContentEditableProps {
+ containerClassName?: string
+ contentEditableClassName?: string
+ placeholderClassName?: string
+ placeholder?: string
+ disabled?: boolean
+ updatedContent?: string
+ onChange: (content: string) => void
+ onKeyUp?: (e: React.KeyboardEvent) => void
+ onKeyDown?: (e: React.KeyboardEvent) => void
+ onFocus?: (e: React.FocusEvent) => void
+ onBlur?: (e: React.FocusEvent) => void
+ onContentExternalUpdate?: (content: string) => void
+}
+
+const ContentEditable: React.FC = ({
+ containerClassName,
+ contentEditableClassName,
+ placeholderClassName,
+ placeholder,
+ disabled,
+ updatedContent,
+ onChange,
+ onKeyUp,
+ onKeyDown,
+ onFocus,
+ onBlur,
+ onContentExternalUpdate,
+}) => {
+ const [content, setContent] = useState("")
+ const divRef = useRef(null)
+
+ useEffect(() => {
+ if (updatedContent !== null && updatedContent !== undefined) {
+ setContent(updatedContent)
+ if (divRef.current) divRef.current.innerText = updatedContent
+ if (onContentExternalUpdate) onContentExternalUpdate(updatedContent)
+ }
+ }, [updatedContent, onContentExternalUpdate])
+
+ useEffect(() => {
+ if (divRef.current) {
+ divRef.current.style.height = "auto"
+ onChange(content)
+ }
+ }, [content, onChange])
+
+ /**
+ * Checks if the caret is on the last line of a contenteditable element
+ * @param element - The HTMLDivElement to check
+ * @returns A boolean indicating whether the caret is on the last line or `false` when the caret is part of a selection
+ */
+ const isCaretOnLastLine = useCallback((element: HTMLDivElement): boolean => {
+ if (element.ownerDocument.activeElement !== element) return false
+
+ // Get the client rect of the current selection
+ const window = element.ownerDocument.defaultView
+
+ if (!window) return false
+
+ const selection = window.getSelection()
+
+ if (!selection || selection.rangeCount === 0) return false
+
+ const originalCaretRange = selection.getRangeAt(0)
+
+ // Bail if there is a selection
+ if (originalCaretRange.toString().length > 0) return false
+
+ const originalCaretRect = originalCaretRange.getBoundingClientRect()
+
+ // Create a range at the end of the last text node
+ const endOfElementRange = document.createRange()
+ endOfElementRange.selectNodeContents(element)
+
+ // The endContainer might not be an actual text node,
+ // try to find the last text node inside
+ let endContainer = endOfElementRange.endContainer
+ let endOffset = 0
+
+ while (endContainer.hasChildNodes() && !(endContainer instanceof Text)) {
+ if (!endContainer.lastChild) continue
+
+ endContainer = endContainer.lastChild
+ endOffset = endContainer instanceof Text ? endContainer.length : 0
+ }
+
+ endOfElementRange.setEnd(endContainer, endOffset)
+ endOfElementRange.setStart(endContainer, endOffset)
+ const endOfElementRect = endOfElementRange.getBoundingClientRect()
+
+ return originalCaretRect.bottom === endOfElementRect.bottom
+ }, [])
+
+ /**
+ * Handles the caret scroll behavior based on keyboard events
+ * @param e - The keyboard event
+ */
+ const handleCaretScroll = useCallback(
+ (e: KeyboardEvent) => {
+ if (!divRef.current) return
+ const focus = divRef.current
+ switch (e.keyCode) {
+ case 38:
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((focus as any).selectionStart === 0) focus.scrollTop = 0
+ break
+ case 13:
+ case 40:
+ if (isCaretOnLastLine(focus)) focus.scrollTop = focus.scrollHeight
+ break
+ default:
+ break
+ }
+ },
+ [isCaretOnLastLine]
+ )
+
+ function handlePasteEvent(e: React.ClipboardEvent) {
+ e.preventDefault()
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const clipboardData = e.clipboardData || (window as any).clipboardData
+ const plainText = clipboardData.getData("text/plain")
+
+ // Get the current selection
+ const sel: Selection | null = window.getSelection()
+ if (sel && sel.rangeCount) {
+ // Get the first range of the selection
+ const range = sel.getRangeAt(0)
+
+ // Delete the contents of the range (this is the selected text)
+ range.deleteContents()
+
+ // Create a new text node containing the pasted text
+ const textNode = document.createTextNode(plainText)
+
+ // Insert the text node into the range, which will replace the selected text
+ range.insertNode(textNode)
+
+ // Move the caret to the end of the new text
+ range.setStartAfter(textNode)
+ sel.removeAllRanges()
+ sel.addRange(range)
+
+ setContent(divRef.current?.innerText ?? "")
+ } else {
+ // If there's no selection, just insert the text at the current caret position
+ insertTextAtCaret(plainText)
+ }
+ }
+
+ /**
+ * Inserts the specified text at the current caret position in the contentEditable element
+ * @param text - The text to be inserted
+ */
+ function insertTextAtCaret(text: string) {
+ if (!divRef.current) return
+ const currentCaretPos = getCaretPosition(divRef.current)
+
+ divRef.current.innerText =
+ divRef.current.innerText.slice(0, currentCaretPos) +
+ text +
+ divRef.current.innerText.slice(currentCaretPos)
+
+ setContent(divRef.current.innerText)
+ divRef.current.scrollTop = divRef.current.scrollHeight
+ setCaretPosition(divRef.current, currentCaretPos + text.length)
+ }
+
+ // Note: setSelectionRange and createTextRange are not supported by contenteditable elements
+
+ /**
+ * Sets the caret position within the contentEditable element
+ * If the element is empty, it will be focused
+ *
+ * @param elem - The contentEditable element
+ * @param pos - The position to set the caret to
+ */
+ function setCaretPosition(elem: HTMLElement, pos: number) {
+ // Create a new range
+ const range = document.createRange()
+
+ // Get the child node of the div
+ const childNode = elem.childNodes[0]
+
+ if (childNode != null) {
+ // Set the range to the correct position within the text
+ range.setStart(childNode, pos)
+ range.setEnd(childNode, pos)
+
+ // Get the selection object
+ const sel: Selection | null = window.getSelection()
+ if (!sel) return
+ // Remove any existing selections
+ sel.removeAllRanges()
+
+ // Add the new range (this will set the cursor position)
+ sel.addRange(range)
+ } else {
+ // If the div is empty, focus it
+ elem.focus()
+ }
+ }
+
+ /**
+ * Retrieves the caret position within the contentEditable element
+ * @param editableDiv - The contentEditable element
+ * @returns The caret position as a number
+ */
+ function getCaretPosition(editableDiv: HTMLElement) {
+ let caretPos = 0,
+ range
+ if (window.getSelection) {
+ const sel: Selection | null = window.getSelection()
+ if (sel && sel.rangeCount) {
+ range = sel.getRangeAt(0)
+ if (range.commonAncestorContainer.parentNode === editableDiv) {
+ caretPos = range.endOffset
+ }
+ }
+ } else if (document.getSelection() && document.getSelection()?.getRangeAt) {
+ range = document.getSelection()?.getRangeAt(0)
+ if (range && range.commonAncestorContainer.parentNode === editableDiv) {
+ const tempEl = document.createElement("span")
+ editableDiv.insertBefore(tempEl, editableDiv.firstChild)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const tempRange: any = range.cloneRange()
+ tempRange.moveToElementText(tempEl)
+ tempRange.setEndPoint("EndToEnd", range)
+ caretPos = tempRange.text.length
+ }
+ }
+ return caretPos
+ }
+
+ function handleKeyDown(e: React.KeyboardEvent) {
+ if (onKeyDown) onKeyDown(e)
+ if ((e.key === "Delete" || e.key === "Backspace") && isAllTextSelected()) {
+ e.preventDefault()
+ if (divRef.current) {
+ divRef.current.innerText = ""
+ setContent("")
+ }
+ }
+ }
+
+ const isAllTextSelected = (): boolean => {
+ const sel: Selection | null = window.getSelection()
+
+ // Matches newline characters that are either followed by another newline
+ // character (\n) or the end of the string ($).
+ const newlineCount = (divRef.current?.innerText.match(/\n(\n|$)/g) || [])
+ .length
+ return sel
+ ? sel.toString().length + newlineCount ===
+ divRef.current?.innerText.length
+ : false
+ }
+
+ useEffect(() => {
+ document.addEventListener("keyup", handleCaretScroll)
+ return () => document.removeEventListener("keyup", handleCaretScroll)
+ }, [handleCaretScroll])
+
+ return (
+