'use client'; import { useMemo, useRef, useState } from 'react'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import type { VideoAsset } from '@/components/video-page/types'; type MentionRange = { start: number; end: number; query: string; }; interface MentionTextareaProps { value: string; onChange: (value: string) => void; assets: VideoAsset[]; placeholder?: string; rows?: number; className?: string; onPaste?: React.ClipboardEventHandler; onKeyDown?: React.KeyboardEventHandler; autoFocus?: boolean; disabled?: boolean; } function findMentionRange(text: string, caret: number): MentionRange | null { const before = text.slice(0, caret); const atIndex = before.lastIndexOf('@'); if (atIndex < 0) return null; const charBeforeAt = atIndex === 0 ? ' ' : before[atIndex - 1]; if (charBeforeAt && !/\s/.test(charBeforeAt)) return null; const query = before.slice(atIndex + 1); if (query.length === 0) { return { start: atIndex, end: caret, query: '' }; } if (/\s|\[|\]|\(|\)/.test(query)) return null; return { start: atIndex, end: caret, query }; } export function MentionTextarea({ value, onChange, assets, placeholder, rows = 2, className, onPaste, onKeyDown, autoFocus, disabled, }: MentionTextareaProps) { const textareaRef = useRef(null); const [mentionRange, setMentionRange] = useState(null); const [activeIndex, setActiveIndex] = useState(0); const searchableAssets = useMemo(() => { return assets .map((asset) => ({ asset, displayNameLower: asset.displayName.toLowerCase(), createdAtMs: Date.parse(asset.createdAt), })) .sort((a, b) => b.createdAtMs - a.createdAtMs); }, [assets]); const filteredAssets = useMemo(() => { if (!mentionRange) return []; const query = mentionRange.query.trim().toLowerCase(); if (!query) return searchableAssets.slice(0, 8).map((entry) => entry.asset); return searchableAssets .filter((entry) => entry.displayNameLower.includes(query)) .map((entry) => entry.asset) .slice(0, 8); }, [mentionRange, searchableAssets]); const closeMentions = () => { setMentionRange(null); setActiveIndex(0); }; const insertAssetMention = (asset: VideoAsset) => { if (!mentionRange || !textareaRef.current) return; const mentionToken = `@[${asset.displayName}](asset:${asset.id}) `; const nextValue = `${value.slice(0, mentionRange.start)}${mentionToken}${value.slice(mentionRange.end)}`; onChange(nextValue); closeMentions(); const nextCursor = mentionRange.start + mentionToken.length; requestAnimationFrame(() => { if (!textareaRef.current) return; textareaRef.current.focus(); textareaRef.current.setSelectionRange(nextCursor, nextCursor); }); }; const handleChange = (event: React.ChangeEvent) => { const nextValue = event.target.value; onChange(nextValue); const caret = event.target.selectionStart ?? nextValue.length; const range = findMentionRange(nextValue, caret); setMentionRange(range); setActiveIndex(0); }; const handleKeyDown: React.KeyboardEventHandler = (event) => { if (mentionRange) { if (filteredAssets.length > 0) { if (event.key === 'ArrowDown') { event.preventDefault(); setActiveIndex((prev) => (prev + 1) % filteredAssets.length); return; } if (event.key === 'ArrowUp') { event.preventDefault(); setActiveIndex((prev) => (prev - 1 + filteredAssets.length) % filteredAssets.length); return; } if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); const selected = filteredAssets[Math.min(activeIndex, filteredAssets.length - 1)]; if (selected) insertAssetMention(selected); return; } } if (event.key === 'Escape') { event.preventDefault(); closeMentions(); return; } } onKeyDown?.(event); }; return (