{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feedback",
  "type": "registry:component",
  "title": "Feedback",
  "description": "A dialog with a well to write in and two actions in the band around it.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/feedback.tsx",
      "type": "registry:component",
      "target": "@components/panelui/feedback.tsx",
      "content": "'use client';\n\n/**\n * Feedback — a dialog whose body is something to write in, and whose actions\n * sit in a band around it.\n *\n * ```tsx\n * <Feedback>\n *   <Feedback.Trigger>Send feedback</Feedback.Trigger>\n *   <Feedback.Content>\n *     <Feedback.Panel>\n *       <Feedback.Title>What should we fix first?</Feedback.Title>\n *       <Feedback.Close />\n *       <Feedback.Field placeholder=\"Tell us what got in your way\" />\n *     </Feedback.Panel>\n *     <Feedback.Footer>\n *       <Feedback.Cancel />\n *       <Feedback.Submit onSubmit={send} />\n *     </Feedback.Footer>\n *   </Feedback.Content>\n * </Feedback>\n * ```\n *\n * ## The panel is a well\n *\n * A dialog that asks for a sentence has two jobs on screen at once: hold what\n * is being written, and offer the two things to do with it. Here the writing\n * surface is set into the dialog and the buttons sit in the band around it.\n * The recess says the field is the page and the rest is its frame, before a\n * word has been read. It is also why the field has no border of its own: an\n * outline inside a well is two edges making one point.\n *\n * ## Nothing is sent empty\n *\n * `Submit` is inert while the field is empty. A dialog that accepts an empty\n * answer produces empty feedback, from somebody who believes they said\n * something. Inert, it drops its fill instead of dimming it, because a tinted\n * accent pill still reads as the thing to press whatever its opacity.\n *\n * ## It does not close itself\n *\n * `Submit` hands the message to `onSubmit` and stays open. Sending has to\n * finish first, and a dialog that closed on the press would take its own error\n * message with it.\n */\nimport { Dialog } from '@base-ui/react/dialog';\nimport { ScrollArea } from '@base-ui/react/scroll-area';\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type * as ReactTypes from 'react';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nconst feedbackVariants = tv({\n  slots: {\n    backdrop:\n      'fixed inset-0 z-50 bg-black/40 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0',\n    viewport: 'fixed inset-0 z-50 grid place-items-center overflow-y-auto p-4',\n    /*\n     * The shell is the popover surface with black mixed into it. The well it\n     * holds is the plain popover surface, so the shell is the darker of the\n     * two in a light theme and in a dark one, which a step on a grey scale\n     * could not promise: those run darker one way and lighter the other.\n     *\n     * The hairline ring is for a dark theme over a dimmed page, where the\n     * shell and what is behind it are both nearly black and the band would\n     * otherwise have no edge.\n     *\n     * 38 and 30 are one corner. The panel's radius is the shell's less the\n     * 8px between them, which is what keeps the two curves concentric.\n     */\n    shell:\n      'relative w-full max-w-sm rounded-[38px] bg-[color-mix(in_oklab,var(--popover),black_5%)] p-2 text-popover-foreground shadow-xl outline-none ring-1 ring-foreground/[0.06] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',\n    popup:\n      'transition-[opacity,scale,translate] duration-200 ease-[cubic-bezier(0.2,0,0,1)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:translate-y-2 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',\n    panel: 'relative flex flex-col gap-4 rounded-[30px] bg-popover p-5 shadow-xs',\n    title: 'pe-9 text-[22px] font-semibold leading-7',\n    // Drawn at 22px against the title's cap height; the `after` is the rest\n    // of a 44px target, which a circle that size would not fit on the line.\n    close:\n      'absolute end-5 top-[23px] inline-flex size-[22px] cursor-pointer items-center justify-center rounded-full bg-foreground/10 text-muted-foreground outline-none transition-colors after:absolute after:-inset-[11px] hover:bg-foreground/15 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring',\n    /*\n     * The field never scrolls itself. It is always as tall as what is written\n     * in it, and the area round it does the scrolling, which is what lets the\n     * bar be a drawn one: a thin rounded thumb that shows while the pointer is\n     * over the field or the text is moving, in place of the browser's own.\n     */\n    field:\n      'block w-full resize-none overflow-hidden bg-transparent p-0 text-[17px] leading-6 text-foreground outline-none placeholder:text-muted-foreground',\n    scroller: 'relative',\n    scrollerViewport: 'rounded-[inherit] outline-none data-has-overflow-y:overscroll-y-contain',\n    // Moved out into the well's padding, so it runs beside the text and not\n    // over the ends of its lines.\n    scrollbar:\n      'my-1 -me-3.5 flex w-1.5 opacity-0 transition-opacity delay-300 data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-100 data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-100',\n    thumb: 'relative flex-1 rounded-full bg-foreground/20',\n    // Body text in the well, set like what is written in the field.\n    description: 'text-[17px] leading-6 text-muted-foreground',\n    // A line under the field: how much is left to write, or what went wrong.\n    message: 'text-sm tabular-nums',\n    rating: 'flex flex-col justify-center gap-3',\n    ratingScores: 'flex justify-between gap-2',\n    ratingScore:\n      'flex size-12 cursor-pointer items-center justify-center rounded-full bg-foreground/10 text-lg font-semibold transition-colors hover:bg-primary hover:text-primary-foreground',\n    ratingEnds: 'flex justify-between text-sm text-muted-foreground',\n    // Held in from the shell's edge further than the panel is. A row the full\n    // width would read as a third edge of the dialog, not as two buttons.\n    footer: 'flex items-center gap-3.5 px-[18px] pb-2.5 pt-4',\n    action:\n      'inline-flex h-11 flex-1 cursor-pointer items-center justify-center rounded-full text-base outline-none transition-[scale,background-color] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.97] motion-reduce:transition-none',\n  },\n  variants: {\n    blur: {\n      true: { backdrop: 'bg-black/20 backdrop-blur-md' },\n    },\n    tone: {\n      /** The one that discards. A tint of the foreground, not a fill. */\n      cancel: { action: 'bg-foreground/10 font-medium text-muted-foreground hover:bg-foreground/15' },\n      /** The one that sends. Filled, and the only filled thing on the dialog. */\n      submit: { action: 'bg-primary font-semibold text-primary-foreground hover:bg-primary/90' },\n    },\n    disabled: {\n      true: { action: 'cursor-not-allowed active:scale-100' },\n    },\n    messageTone: {\n      muted: { message: 'text-muted-foreground' },\n      destructive: { message: 'text-destructive' },\n    },\n  },\n  compoundVariants: [\n    // Fainter than Cancel's own tint on purpose. Matched, they would read as\n    // two Cancels, not as one live action and one that is not ready.\n    {\n      tone: 'submit',\n      disabled: true,\n      class: {\n        action: 'bg-foreground/[0.04] font-medium text-muted-foreground/50 hover:bg-foreground/[0.04]',\n      },\n    },\n    { tone: 'cancel', disabled: true, class: { action: 'opacity-45' } },\n  ],\n});\n\ninterface FeedbackContextValue {\n  open: boolean;\n  setOpen(open: boolean): void;\n  /** What is in the field, so Submit can refuse to send nothing. */\n  value: string;\n  setValue(value: string): void;\n  /** The send Submit would perform, for the field's keyboard shortcut. */\n  submit: React.RefObject<(() => void) | null>;\n  /** Set by `Feedback.Content`, so the parts inside know which elements to be. */\n  inline: boolean;\n}\n\nconst FeedbackContext = createContext<FeedbackContextValue | null>(null);\n\nfunction useFeedback(part: string): FeedbackContextValue {\n  const context = useContext(FeedbackContext);\n  if (!context) throw new Error(`${part} must be used inside a <Feedback>.`);\n  return context;\n}\n\nexport interface FeedbackProps {\n  children?: ReactNode;\n  /** Whether the dialog is open, when the caller holds that. */\n  open?: boolean;\n  /** Called when the dialog asks to open or close. */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether it starts open, when the caller does not hold it. */\n  defaultOpen?: boolean;\n  /** The message, when the caller holds it. Leave unset to let the field keep it. */\n  value?: string;\n  /** The message to start with, when the caller does not hold it. */\n  defaultValue?: string;\n  /** Called with the message each time it changes. */\n  onValueChange?: (value: string) => void;\n  /**\n   * Whether a click outside or the Escape key closes the dialog. Turn it off\n   * for a message that would be lost by a stray click; Cancel and the close\n   * button still work.\n   */\n  dismissible?: boolean;\n}\n\nfunction FeedbackRoot({\n  children,\n  open,\n  onOpenChange,\n  defaultOpen = false,\n  value,\n  defaultValue = '',\n  onValueChange,\n  dismissible = true,\n}: FeedbackProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const resolvedOpen = open ?? internalOpen;\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const resolvedValue = value ?? internalValue;\n  const submit = useRef<(() => void) | null>(null);\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [open, onOpenChange]\n  );\n\n  const setValue = useCallback(\n    (next: string) => {\n      if (value === undefined) setInternalValue(next);\n      onValueChange?.(next);\n    },\n    [value, onValueChange]\n  );\n\n  const context = useMemo(\n    () => ({ open: resolvedOpen, setOpen, value: resolvedValue, setValue, submit, inline: false }),\n    [resolvedOpen, setOpen, resolvedValue, setValue]\n  );\n\n  return (\n    <FeedbackContext.Provider value={context}>\n      <Dialog.Root\n        open={resolvedOpen}\n        disablePointerDismissal={!dismissible}\n        onOpenChange={(next, details) => {\n          // The primitive has a switch for a press outside and none for\n          // Escape, so that one is refused here.\n          if (!next && !dismissible && details.reason === 'escape-key') return;\n          setOpen(next);\n        }}\n      >\n        {children}\n      </Dialog.Root>\n    </FeedbackContext.Provider>\n  );\n}\nFeedbackRoot.displayName = 'Feedback';\n\nexport interface FeedbackTriggerProps extends Omit<ComponentProps<'button'>, 'children'> {\n  children?: ReactNode;\n  /**\n   * Your own element to be the trigger, such as a styled button. It is given\n   * the click handler and the attributes that tie it to the dialog.\n   */\n  render?: ReactElement;\n}\n\n/** What opens the dialog. A plain button unless `render` supplies one. */\nfunction FeedbackTrigger({ render, children, ...props }: FeedbackTriggerProps) {\n  return (\n    <Dialog.Trigger render={render} {...props}>\n      {children}\n    </Dialog.Trigger>\n  );\n}\nFeedbackTrigger.displayName = 'Feedback.Trigger';\n\nexport interface FeedbackContentProps extends Omit<ComponentProps<'div'>, 'children'> {\n  children?: ReactNode;\n  /**\n   * Draw it in the page instead of over it: no backdrop, no trigger, always\n   * there. For a feedback box that is part of the page, such as the end of an\n   * article. `open` is ignored, and Cancel and the close button only report\n   * through `onOpenChange`.\n   */\n  inline?: boolean;\n  /** Frost the page behind the dialog instead of only dimming it. */\n  blur?: boolean;\n}\n\n/** The shell: the darker band, and everything set in it. */\nfunction FeedbackContent({ inline = false, blur = false, className, children, ...props }: FeedbackContentProps) {\n  const feedback = useFeedback('Feedback.Content');\n  const slots = feedbackVariants({ blur });\n  const context = useMemo(() => ({ ...feedback, inline }), [feedback, inline]);\n\n  if (inline) {\n    return (\n      <FeedbackContext.Provider value={context}>\n        <div className={slots.shell({ className })} {...props}>\n          {children}\n        </div>\n      </FeedbackContext.Provider>\n    );\n  }\n\n  return (\n    <Dialog.Portal>\n      <Dialog.Backdrop className={slots.backdrop()} />\n      <Dialog.Viewport className={slots.viewport()}>\n        <Dialog.Popup className={slots.shell({ className: [slots.popup(), className] })} {...props}>\n          {/* The popup is portalled out from under the provider, so the\n              context is given again for the parts inside it. */}\n          <FeedbackContext.Provider value={context}>{children}</FeedbackContext.Provider>\n        </Dialog.Popup>\n      </Dialog.Viewport>\n    </Dialog.Portal>\n  );\n}\nFeedbackContent.displayName = 'Feedback.Content';\n\nexport interface FeedbackPanelProps extends ComponentProps<'div'> {}\n\n/** The well set into the shell: the title, the close button and the field. */\nfunction FeedbackPanel({ className, render, ...props }: FeedbackPanelProps & RenderProp<'div'>) {\n  const slots = feedbackVariants();\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ className: slots.panel({ className }) }, props),\n  });\n}\nFeedbackPanel.displayName = 'Feedback.Panel';\n\nexport interface FeedbackTitleProps extends ComponentProps<'h2'> {}\n\n/** The question. In a dialog it is also what the dialog is announced as. */\nfunction FeedbackTitle({ className, ...props }: FeedbackTitleProps) {\n  const { inline } = useFeedback('Feedback.Title');\n  const slots = feedbackVariants();\n  if (inline) return <h2 className={slots.title({ className })} {...props} />;\n  return <Dialog.Title className={slots.title({ className })} {...props} />;\n}\nFeedbackTitle.displayName = 'Feedback.Title';\n\nexport interface FeedbackCloseProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** How the button announces itself to a screen reader. */\n  label?: string;\n}\n\n/** The ✕ in the panel's corner. */\nfunction FeedbackClose({\n  label = 'Close',\n  className,\n  onClick,\n  render,\n  ...props\n}: FeedbackCloseProps & RenderProp<'button'>) {\n  const { setOpen } = useFeedback('Feedback.Close');\n  const slots = feedbackVariants();\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        type: 'button',\n        'aria-label': label,\n        className: slots.close({ className }),\n        onClick: (event: ReactTypes.MouseEvent<HTMLButtonElement>) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) setOpen(false);\n        },\n        children: (\n          <>\n            <svg\n              viewBox=\"0 0 10 10\"\n              aria-hidden=\"true\"\n              className=\"size-2.5\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.6\"\n              strokeLinecap=\"round\"\n            >\n              <path d=\"M2 2l6 6M8 2l-6 6\" />\n            </svg>\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nFeedbackClose.displayName = 'Feedback.Close';\n\nexport interface FeedbackFieldProps extends Omit<ComponentProps<'textarea'>, 'value' | 'defaultValue' | 'onChange'> {\n  /** The message. Leave unset to let the dialog hold it. */\n  value?: string;\n  /** Called with the message each time it changes. */\n  onValueChange?: (value: string) => void;\n  /** Room to write in, in pixels, before the field starts to grow. */\n  minHeight?: number;\n  /** How tall the field may grow, in pixels, before the area round it scrolls. */\n  maxHeight?: number;\n}\n\n/**\n * What is being written. No border and no background of its own: it is\n * already inside a well.\n */\nfunction FeedbackField({\n  value,\n  onValueChange,\n  minHeight = 200,\n  maxHeight = 360,\n  className,\n  style,\n  onKeyDown,\n  ref,\n  'aria-label': ariaLabel = 'Your message',\n  ...props\n}: FeedbackFieldProps) {\n  const feedback = useFeedback('Feedback.Field');\n  const slots = feedbackVariants();\n  const field = useRef<HTMLTextAreaElement | null>(null);\n  const text = value ?? feedback.value;\n\n  // As tall as what is written, and never shorter than the room it starts\n  // with. Measured with the height let go first, or a field that has grown\n  // could never shrink.\n  useLayoutEffect(() => {\n    const node = field.current;\n    if (!node) return;\n    node.style.height = 'auto';\n    node.style.height = `${Math.max(minHeight, node.scrollHeight)}px`;\n  }, [text, minHeight]);\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    // Enter makes a new line, as it does in any text box. Command or Control\n    // with it sends, which is the shortcut for it everywhere else.\n    if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n      event.preventDefault();\n      feedback.submit.current?.();\n    }\n  };\n\n  return (\n    <ScrollArea.Root className={slots.scroller()}>\n      <ScrollArea.Viewport className={slots.scrollerViewport()} style={{ maxHeight }}>\n        <ScrollArea.Content style={{ minWidth: 0 }}>\n          <textarea\n            ref={(node) => {\n              field.current = node;\n              if (typeof ref === 'function') ref(node);\n              else if (ref) ref.current = node;\n            }}\n            aria-label={ariaLabel}\n            value={text}\n            onChange={(event) => {\n              feedback.setValue(event.target.value);\n              onValueChange?.(event.target.value);\n            }}\n            onKeyDown={handleKeyDown}\n            className={slots.field({ className })}\n            style={{ minHeight, ...style }}\n            {...props}\n          />\n        </ScrollArea.Content>\n      </ScrollArea.Viewport>\n      <ScrollArea.Scrollbar orientation=\"vertical\" className={slots.scrollbar()}>\n        <ScrollArea.Thumb className={slots.thumb()} />\n      </ScrollArea.Scrollbar>\n    </ScrollArea.Root>\n  );\n}\nFeedbackField.displayName = 'Feedback.Field';\n\nexport interface FeedbackDescriptionProps extends ComponentProps<'p'> {}\n\n/**\n * Body text in the panel, in place of the field: a thank-you once the message\n * has gone. In a dialog it is also what the dialog is described as.\n */\nfunction FeedbackDescription({ className, ...props }: FeedbackDescriptionProps) {\n  const { inline } = useFeedback('Feedback.Description');\n  const slots = feedbackVariants();\n  if (inline) return <p className={slots.description({ className })} {...props} />;\n  return <Dialog.Description className={slots.description({ className })} {...props} />;\n}\nFeedbackDescription.displayName = 'Feedback.Description';\n\nexport interface FeedbackMessageProps extends ComponentProps<'p'> {\n  /**\n   * `muted` for a running note, read out politely as it changes: how much is\n   * left to write. `destructive` for a failure, announced at once. Default\n   * `muted`.\n   */\n  tone?: 'muted' | 'destructive';\n}\n\n/** A short line under the field. Figures in it keep their width as they change. */\nfunction FeedbackMessage({ tone = 'muted', className, render, ...props }: FeedbackMessageProps & RenderProp<'p'>) {\n  const slots = feedbackVariants({ messageTone: tone });\n  return useRender({\n    defaultTagName: 'p',\n    render,\n    props: mergeProps(\n      {\n        role: tone === 'destructive' ? 'alert' : undefined,\n        'aria-live': tone === 'destructive' ? undefined : 'polite',\n        className: slots.message({ className }),\n      },\n      props\n    ),\n  });\n}\nFeedbackMessage.displayName = 'Feedback.Message';\n\nexport interface FeedbackRatingProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** The highest score. The scores run from 1 to this. Default 5. */\n  max?: number;\n  /** Called with the score that was pressed. */\n  onValueChange?: (value: number) => void;\n  /** The word under the lowest score. */\n  startLabel?: ReactNode;\n  /** The word under the highest score. */\n  endLabel?: ReactNode;\n  /**\n   * The least height it takes, in pixels. Match the field's `minHeight` on the\n   * step that follows, so the dialog keeps its size between the two.\n   */\n  minHeight?: number;\n}\n\n/**\n * A row of round buttons, one per score, with a word under each end. Use it\n * as the first step of a dialog, and ask for the words once a score is in.\n */\nfunction FeedbackRating({\n  max = 5,\n  onValueChange,\n  startLabel,\n  endLabel,\n  minHeight,\n  className,\n  style,\n  render,\n  ...props\n}: FeedbackRatingProps & RenderProp<'div'>) {\n  const slots = feedbackVariants();\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        className: slots.rating({ className }),\n        style: { minHeight, ...style },\n        children: (\n          <>\n            <div className={slots.ratingScores()}>\n              {Array.from({ length: max }, (_, index) => index + 1).map((value) => (\n                <button\n                  key={value}\n                  type=\"button\"\n                  aria-label={`${value} out of ${max}`}\n                  onClick={() => onValueChange?.(value)}\n                  className={slots.ratingScore()}\n                >\n                  {value}\n                </button>\n              ))}\n            </div>\n            {startLabel !== undefined || endLabel !== undefined ? (\n              <div className={slots.ratingEnds()}>\n                <span>{startLabel}</span>\n                <span>{endLabel}</span>\n              </div>\n            ) : null}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nFeedbackRating.displayName = 'Feedback.Rating';\n\nexport interface FeedbackFooterProps extends ComponentProps<'div'> {}\n\n/** The action row, in the band under the panel. */\nfunction FeedbackFooter({ className, render, ...props }: FeedbackFooterProps & RenderProp<'div'>) {\n  const slots = feedbackVariants();\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ className: slots.footer({ className }) }, props),\n  });\n}\nFeedbackFooter.displayName = 'Feedback.Footer';\n\nexport interface FeedbackActionProps extends ComponentProps<'button'> {}\n\n/** Discards and closes. Call `preventDefault` in `onClick` to keep it open. */\nfunction FeedbackCancel({\n  children = 'Cancel',\n  className,\n  disabled,\n  onClick,\n  render,\n  ...props\n}: FeedbackActionProps & RenderProp<'button'>) {\n  const { setOpen } = useFeedback('Feedback.Cancel');\n  const slots = feedbackVariants({ tone: 'cancel', disabled: !!disabled });\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        type: 'button',\n        disabled: disabled,\n        className: slots.action({ className }),\n        onClick: (event: ReactTypes.MouseEvent<HTMLButtonElement>) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) setOpen(false);\n        },\n        children: <>{children}</>,\n      },\n      props\n    ),\n  });\n}\nFeedbackCancel.displayName = 'Feedback.Cancel';\n\nexport interface FeedbackSubmitProps extends Omit<FeedbackActionProps, 'onSubmit'> {\n  /**\n   * Called with the message. The dialog stays open: close it yourself once\n   * the message has been sent, so a failure still has somewhere to be shown.\n   */\n  onSubmit?: (value: string) => void;\n}\n\n/** Sends. Inert while the field is empty, unless `disabled` says otherwise. */\nfunction FeedbackSubmit({\n  children = 'Submit',\n  className,\n  disabled,\n  onClick,\n  onSubmit,\n  render,\n  ...props\n}: FeedbackSubmitProps & RenderProp<'button'>) {\n  const feedback = useFeedback('Feedback.Submit');\n  // Empty feedback is worse than none: it is sent by somebody who believes\n  // they said something.\n  const inert = disabled ?? feedback.value.trim().length === 0;\n  const slots = feedbackVariants({ tone: 'submit', disabled: inert });\n\n  // What the field's shortcut calls. Kept current on every render, so it\n  // sends what is in the field now and not what was there when it mounted.\n  feedback.submit.current = inert ? null : () => onSubmit?.(feedback.value);\n\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        type: 'button',\n        disabled: inert,\n        className: slots.action({ className }),\n        onClick: (event: ReactTypes.MouseEvent<HTMLButtonElement>) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) onSubmit?.(feedback.value);\n        },\n        children: <>{children}</>,\n      },\n      props\n    ),\n  });\n}\nFeedbackSubmit.displayName = 'Feedback.Submit';\n\nexport const Feedback = Object.assign(FeedbackRoot, {\n  Trigger: FeedbackTrigger,\n  Content: FeedbackContent,\n  Panel: FeedbackPanel,\n  Title: FeedbackTitle,\n  Close: FeedbackClose,\n  Field: FeedbackField,\n  Description: FeedbackDescription,\n  Message: FeedbackMessage,\n  Rating: FeedbackRating,\n  Footer: FeedbackFooter,\n  Cancel: FeedbackCancel,\n  Submit: FeedbackSubmit,\n});\n\n/* The parts by name, for a Server Component, which cannot dot into a client\n   module. `<FeedbackContent>` there is `<Feedback.Content>` everywhere else. */\nexport {\n  FeedbackTrigger,\n  FeedbackContent,\n  FeedbackPanel,\n  FeedbackTitle,\n  FeedbackClose,\n  FeedbackField,\n  FeedbackDescription,\n  FeedbackMessage,\n  FeedbackRating,\n  FeedbackFooter,\n  FeedbackCancel,\n  FeedbackSubmit,\n};\n"
    }
  ]
}
