{
  "name": "tsf-inline-edit",
  "type": "registry:ui",
  "dependencies": [
    "lucide-react",
    "react"
  ],
  "registryDependencies": [
    "field",
    "input",
    "popover",
    "textarea",
    "tsf-form-context"
  ],
  "files": [
    {
      "path": "./items/tanstack-form/inline-edit/component.tsx",
      "type": "registry:ui",
      "target": "./components/ui/shuip/tanstack-form/inline-edit.tsx",
      "content": "'use client';\n\nimport { InfoIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { Field, FieldLabel } from '@/components/ui/field';\nimport { Input } from '@/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';\nimport { Textarea } from '@/components/ui/textarea';\nimport { cn } from '@/lib/utils';\n\ntype InlineEditSize = 'sm' | 'default' | 'title';\ntype InlineEditVariant = 'ghost' | 'boxed';\ntype InlineEditInput = 'text' | 'textarea';\n\ninterface InlineEditProps {\n  value: string;\n  onCommit: (next: string) => Promise<string | undefined> | string | undefined;\n  input?: InlineEditInput;\n  variant?: InlineEditVariant;\n  size?: InlineEditSize;\n  placeholder?: string;\n  canEdit?: boolean;\n  children?: (api: {\n    value: string;\n    setValue: (v: string) => void;\n    commit: (next?: string) => void;\n    cancel: () => void;\n    className: string;\n    autoFocus: true;\n  }) => React.ReactNode;\n}\n\ntype State =\n  | { kind: 'reading' }\n  | { kind: 'editing'; draft: string; error?: string }\n  | { kind: 'saving'; draft: string };\n\nconst sizeClasses: Record<InlineEditSize, string> = {\n  sm: 'px-1.5 py-0.5 text-xs',\n  default: 'px-2 py-1 text-sm',\n  title: 'px-2 py-1 text-3xl font-semibold tracking-tight',\n};\n\nconst variantClasses: Record<InlineEditVariant, string> = {\n  ghost:\n    'border border-transparent bg-transparent hover:bg-muted focus-visible:bg-muted dark:bg-transparent dark:hover:bg-muted',\n  boxed: 'border border-input bg-transparent shadow-xs focus-visible:border-ring',\n};\n\nfunction InlineEdit({\n  value,\n  onCommit,\n  input = 'text',\n  variant = 'ghost',\n  size = 'default',\n  placeholder = '—',\n  canEdit = true,\n  children,\n}: InlineEditProps) {\n  const [state, setState] = React.useState<State>({ kind: 'reading' });\n  const errorId = React.useId();\n\n  const fieldClassName = cn(\n    'w-full rounded-md text-left outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50',\n    sizeClasses[size],\n    variantClasses[variant],\n  );\n\n  const startEdit = () => {\n    if (canEdit) setState({ kind: 'editing', draft: value });\n  };\n\n  const cancel = () => setState({ kind: 'reading' });\n\n  const setDraft = (draft: string) => setState((s) => (s.kind === 'editing' ? { kind: 'editing', draft } : s));\n\n  const commit = async (override?: string) => {\n    if (state.kind !== 'editing') return;\n    const draft = override ?? state.draft;\n    if (draft === value) {\n      setState({ kind: 'reading' });\n      return;\n    }\n    setState({ kind: 'saving', draft });\n    const error = await onCommit(draft);\n    setState(error ? { kind: 'editing', draft, error } : { kind: 'reading' });\n  };\n\n  const displayedError = state.kind === 'editing' ? state.error : undefined;\n  const isSaving = state.kind === 'saving';\n\n  let content: React.ReactNode;\n  if (state.kind === 'reading') {\n    const isEmpty = value === '';\n    content = (\n      <span\n        data-slot='inline-edit'\n        role={canEdit ? 'button' : undefined}\n        tabIndex={canEdit ? 0 : undefined}\n        onClick={canEdit ? startEdit : undefined}\n        onKeyDown={\n          canEdit\n            ? (e) => {\n                if (e.key === 'Enter' || e.key === ' ') {\n                  e.preventDefault();\n                  startEdit();\n                }\n              }\n            : undefined\n        }\n        className={cn(\n          fieldClassName,\n          'block',\n          canEdit ? 'cursor-text' : 'cursor-default',\n          input === 'textarea' && 'whitespace-pre-wrap',\n          isEmpty && 'text-muted-foreground',\n        )}\n      >\n        {isEmpty ? placeholder : value}\n      </span>\n    );\n  } else {\n    const { draft } = state;\n    const editorClassName = cn(\n      fieldClassName,\n      'h-auto min-w-0',\n      variant === 'ghost' && 'shadow-none',\n      input === 'textarea' && 'field-sizing-content min-h-0',\n    );\n    const handleKeyDown: React.KeyboardEventHandler = (e) => {\n      if (e.key === 'Escape') {\n        e.preventDefault();\n        cancel();\n      }\n      if (e.key === 'Enter' && input === 'text') {\n        e.preventDefault();\n        void commit();\n      }\n    };\n\n    content = children ? (\n      children({\n        value: draft,\n        setValue: setDraft,\n        commit: (next?: string) => void commit(next),\n        cancel,\n        className: editorClassName,\n        autoFocus: true,\n      })\n    ) : input === 'textarea' ? (\n      <Textarea\n        autoFocus\n        value={draft}\n        disabled={isSaving}\n        aria-invalid={!!displayedError}\n        aria-describedby={displayedError ? errorId : undefined}\n        onChange={(e) => setDraft(e.target.value)}\n        onBlur={() => void commit()}\n        onKeyDown={handleKeyDown}\n        className={editorClassName}\n      />\n    ) : (\n      <Input\n        autoFocus\n        type='text'\n        value={draft}\n        disabled={isSaving}\n        aria-invalid={!!displayedError}\n        aria-describedby={displayedError ? errorId : undefined}\n        onChange={(e) => setDraft(e.target.value)}\n        onBlur={() => void commit()}\n        onKeyDown={handleKeyDown}\n        className={editorClassName}\n      />\n    );\n  }\n\n  return (\n    <div className='flex w-full flex-col gap-1'>\n      <div className='flex items-center gap-2'>\n        <div className='min-w-0 flex-1'>{content}</div>\n        {isSaving && <span aria-hidden className='size-1.5 shrink-0 animate-pulse rounded-full bg-primary/60' />}\n      </div>\n      {displayedError && (\n        <p id={errorId} className='text-xs text-destructive'>\n          {displayedError}\n        </p>\n      )}\n    </div>\n  );\n}\n\nexport interface InlineEditFieldProps\n  extends Pick<InlineEditProps, 'input' | 'variant' | 'size' | 'placeholder' | 'canEdit' | 'children'> {\n  label?: string;\n  description?: string;\n  orientation?: 'vertical' | 'horizontal';\n}\n\nexport function InlineEditField({ label, description, orientation = 'vertical', ...props }: InlineEditFieldProps) {\n  const field = useFieldContext<string>();\n  const { isValid } = field.state.meta;\n\n  const handleCommit = async (next: string) => {\n    field.handleChange(next);\n    if (!field.state.meta.isValid) {\n      const first = field.state.meta.errors[0];\n      return typeof first === 'string' ? first : (first?.message ?? 'Invalid value');\n    }\n    await field.form.handleSubmit();\n    return undefined;\n  };\n\n  return (\n    <Field\n      orientation={orientation}\n      data-invalid={!isValid}\n      className={cn('gap-2', orientation === 'horizontal' && '[&>[data-slot=field-label]]:flex-none')}\n    >\n      {label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}\n      <div className={cn('flex items-center gap-2', orientation === 'horizontal' && 'flex-1')}>\n        <InlineEdit {...props} value={field.state.value ?? ''} onCommit={handleCommit} />\n        {description && (\n          <Popover>\n            <PopoverTrigger aria-label='Info' className='text-muted-foreground transition-colors hover:text-foreground'>\n              <InfoIcon className='size-4' />\n            </PopoverTrigger>\n            <PopoverContent className='w-64 text-sm'>{description}</PopoverContent>\n          </Popover>\n        )}\n      </div>\n    </Field>\n  );\n}\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
