{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-list-formatting",
  "title": "useListFormatting",
  "author": "Stickies <https://stickies.live>",
  "description": "Bullets and numbered lists in a plain textarea: '- ' starts a bullet and Return continues the list.",
  "files": [
    {
      "path": "registry/stickies/hooks/use-list-formatting.ts",
      "content": "\"use client\"\n\nimport { useLayoutEffect, useRef } from \"react\"\n\n// Lists in a plain textarea, as in Apple's Notes: \"- \" or \"* \" at the start of a line becomes\n// a bullet, Return carries the list on, and Return on an empty item ends it. Numbered lists\n// (\"1. \") keep counting. The bullet is a character, so the text stays plain.\n\nexport const BULLET = \"• \"\n\nexport interface TextEdit {\n  text: string\n  caret: number\n}\n\nconst lineStart = (text: string, at: number) => text.lastIndexOf(\"\\n\", at - 1) + 1\n\n/** After a keystroke: \"- \" or \"* \" just typed at the start of a line becomes a bullet. */\nexport function autoBullet(text: string, caret: number): TextEdit | null {\n  const start = lineStart(text, caret)\n  const typed = text.slice(start, caret)\n  if (typed !== \"- \" && typed !== \"* \") return null\n  return { text: text.slice(0, start) + BULLET + text.slice(caret), caret: start + BULLET.length }\n}\n\n/** Return on a list's line: the next item, or on an empty one, the end of the list. Null when not in a list. */\nexport function continueList(text: string, from: number, to: number): TextEdit | null {\n  const start = lineStart(text, from)\n  const line = text.slice(start, from)\n  const numbered = /^(\\d+)\\. /.exec(line)\n  const marker = line.startsWith(BULLET) ? BULLET : numbered ? numbered[0] : null\n  if (!marker) return null\n  if (line === marker && text.slice(to).split(\"\\n\")[0] === \"\") {\n    return { text: text.slice(0, start) + text.slice(to), caret: start }\n  }\n  const next = numbered ? `${Number(numbered[1]) + 1}. ` : BULLET\n  const inserted = `\\n${next}`\n  return { text: text.slice(0, from) + inserted + text.slice(to), caret: from + inserted.length }\n}\n\n/**\n * Bullets and numbered lists for a controlled textarea. Spread the returned props on it;\n * `onTextChange` gets the new text, formatted when a list was started or carried on.\n */\nexport function useListFormatting(value: string, onTextChange: (text: string) => void) {\n  const ref = useRef<HTMLTextAreaElement>(null)\n  const caretAfter = useRef<number | null>(null)\n\n  // Puts the caret after a bullet or a new item once the change is on screen.\n  useLayoutEffect(() => {\n    const el = ref.current\n    const at = caretAfter.current\n    if (!el || at === null) return\n    caretAfter.current = null\n    el.setSelectionRange(at, at)\n  }, [value])\n\n  return {\n    ref,\n    value,\n    onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {\n      const native = e.nativeEvent as InputEvent\n      const bulleted = native.inputType === \"insertText\" && native.data === \" \" ? autoBullet(e.target.value, e.target.selectionStart) : null\n      if (bulleted) caretAfter.current = bulleted.caret\n      onTextChange(bulleted?.text ?? e.target.value)\n    },\n    onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n      if (e.key !== \"Enter\" || e.shiftKey || e.altKey || e.metaKey || e.ctrlKey || e.nativeEvent.isComposing) return\n      const el = e.currentTarget\n      const next = continueList(el.value, el.selectionStart, el.selectionEnd)\n      if (!next) return\n      e.preventDefault()\n      caretAfter.current = next.caret\n      onTextChange(next.text)\n    },\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "docs": "See https://stickies.live/docs#use-list-formatting for usage.",
  "categories": [
    "stickies"
  ],
  "type": "registry:hook"
}