{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feedback-widget",
  "title": "Feedback Widget",
  "description": "Edge tab opening a feedback form with a five-point rating.",
  "dependencies": ["lucide-react"],
  "registryDependencies": [
    "https://indiacn.in/r/theme.json",
    "https://indiacn.in/r/typography.json",
    "https://indiacn.in/r/button.json",
    "https://indiacn.in/r/input.json",
    "https://indiacn.in/r/textarea.json",
    "https://indiacn.in/r/label.json",
    "https://indiacn.in/r/modal.json"
  ],
  "files": [
    {
      "path": "components/ui/feedback-widget.tsx",
      "content": "'use client';\n\nimport { Angry, Frown, Laugh, Meh, Smile } from 'lucide-react';\nimport { ComponentProps, FormEvent, ReactNode, useCallback, useId, useState } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Input, InputMessage } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport {\n  Modal,\n  ModalBody,\n  ModalContent,\n  ModalDescription,\n  ModalFooter,\n  ModalHeader,\n  ModalTitle,\n} from '@/components/ui/modal';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Body3, Label2 } from '@/components/ui/typography';\nimport { cn } from '@/lib/utils';\n\n/*\n * UX4G feedback widget: a vertical tab pinned to the right edge opening a form\n * with a five-point rating, a message and a character counter.\n *\n * The kit draws the rating as illustrated yellow emoji. Those would be image\n * assets a registry component cannot ship, and they cannot be recoloured or\n * themed, so this uses the matching line faces instead.\n */\nconst RATINGS = [\n  { value: 1, label: 'Very dissatisfied', Icon: Angry },\n  { value: 2, label: 'Dissatisfied', Icon: Frown },\n  { value: 3, label: 'Neutral', Icon: Meh },\n  { value: 4, label: 'Satisfied', Icon: Smile },\n  { value: 5, label: 'Very satisfied', Icon: Laugh },\n] as const;\n\ninterface IFeedbackRatingProps {\n  value: number | null;\n  onChange: (value: number) => void;\n  className?: string;\n}\n\n/**\n * Five-point rating scale.\n *\n * A radio group rather than five buttons, so arrow keys move between options\n * and a screen reader announces \"3 of 5\" instead of five unrelated controls.\n */\nfunction FeedbackRating({ value, onChange, className }: IFeedbackRatingProps) {\n  return (\n    <div role='radiogroup' aria-label='Rating' className={cn('flex gap-3', className)}>\n      {RATINGS.map(({ value: option, label, Icon }) => (\n        <FeedbackRatingOption\n          key={option}\n          option={option}\n          label={label}\n          Icon={Icon}\n          selected={value === option}\n          onChange={onChange}\n        />\n      ))}\n    </div>\n  );\n}\n\n/** One face in the scale. Split out so its handler is stable. */\nfunction FeedbackRatingOption({\n  option,\n  label,\n  Icon,\n  selected,\n  onChange,\n}: {\n  option: number;\n  label: string;\n  Icon: typeof Angry;\n  selected: boolean;\n  onChange: (value: number) => void;\n}) {\n  const handleClick = useCallback(() => onChange(option), [onChange, option]);\n\n  return (\n    <button\n      type='button'\n      role='radio'\n      aria-checked={selected}\n      aria-label={label}\n      onClick={handleClick}\n      className={cn(\n        'focus-visible:shadow-focus-primary flex size-12 cursor-pointer items-center justify-center rounded-full border transition-colors focus-visible:outline-none',\n        selected\n          ? 'border-primary bg-primary-100 text-primary-800'\n          : 'hover:border-primary hover:text-primary border-neutral-200 text-neutral-600',\n      )}\n    >\n      <Icon className='size-6' aria-hidden />\n    </button>\n  );\n}\n\ninterface IFeedbackValue {\n  name: string;\n  email: string;\n  rating: number | null;\n  message: string;\n}\n\ninterface IFeedbackWidgetProps extends Omit<ComponentProps<'div'>, 'onSubmit' | 'children'> {\n  onSubmit: (value: IFeedbackValue) => void | Promise<void>;\n  title?: string;\n  description?: string;\n  maxLength?: number;\n  /** Replaces the tab label. */\n  triggerLabel?: string;\n  footer?: ReactNode;\n}\n\n/**\n * Feedback tab and form.\n *\n * Rating and message are required, matching the asterisks in the kit. The\n * form refuses to submit without them and says which one is missing rather\n * than disabling the button with no explanation.\n */\nfunction FeedbackWidget({\n  className,\n  onSubmit,\n  title = 'We value your feedback',\n  description = 'Share your thoughts to help us improve this service.',\n  maxLength = 200,\n  triggerLabel = 'Feedback',\n  footer,\n  ...props\n}: IFeedbackWidgetProps) {\n  const [open, setOpen] = useState(false);\n  const [value, setValue] = useState<IFeedbackValue>({\n    name: '',\n    email: '',\n    rating: null,\n    message: '',\n  });\n  const [showErrors, setShowErrors] = useState(false);\n  const formId = useId();\n\n  const missingRating = value.rating === null;\n  const missingMessage = value.message.trim() === '';\n\n  const setName = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) =>\n      setValue(current => ({ ...current, name: e.target.value })),\n    [],\n  );\n  const setEmail = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) =>\n      setValue(current => ({ ...current, email: e.target.value })),\n    [],\n  );\n  const setMessage = useCallback(\n    (e: React.ChangeEvent<HTMLTextAreaElement>) =>\n      setValue(current => ({ ...current, message: e.target.value })),\n    [],\n  );\n  const setRating = useCallback(\n    (rating: number) => setValue(current => ({ ...current, rating })),\n    [],\n  );\n  const close = useCallback(() => setOpen(false), []);\n  const openForm = useCallback(() => setOpen(true), []);\n\n  const handleSubmit = useCallback(\n    (e: FormEvent) => {\n      e.preventDefault();\n      if (missingRating || missingMessage) {\n        setShowErrors(true);\n        return;\n      }\n      void onSubmit(value);\n      setOpen(false);\n      setShowErrors(false);\n      setValue({ name: '', email: '', rating: null, message: '' });\n    },\n    [missingRating, missingMessage, onSubmit, value],\n  );\n\n  return (\n    <div className={cn('fixed top-1/2 right-0 z-40 -translate-y-1/2', className)} {...props}>\n      <Modal open={open} onOpenChange={setOpen}>\n        <button\n          type='button'\n          onClick={openForm}\n          aria-label={`${triggerLabel} — open the feedback form`}\n          className='bg-primary text-primary-foreground focus-visible:shadow-focus-primary w-10 cursor-pointer rounded-l-md py-5 transition-opacity hover:opacity-90 focus-visible:outline-none'\n        >\n          <Label2 className='[text-orientation:mixed] [writing-mode:vertical-rl]'>\n            {triggerLabel}\n          </Label2>\n        </button>\n\n        <ModalContent className='max-w-[560px]'>\n          <ModalHeader>\n            <ModalTitle>{title}</ModalTitle>\n          </ModalHeader>\n          <ModalBody>\n            <ModalDescription className='mb-6'>{description}</ModalDescription>\n\n            <form id={formId} onSubmit={handleSubmit} className='flex flex-col gap-5'>\n              <div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>\n                <div>\n                  <Label htmlFor={`${formId}-name`} className='mb-2'>\n                    Full name\n                  </Label>\n                  <Input\n                    id={`${formId}-name`}\n                    value={value.name}\n                    onChange={setName}\n                    placeholder='Enter name'\n                  />\n                </div>\n                <div>\n                  <Label htmlFor={`${formId}-email`} className='mb-2'>\n                    Email\n                  </Label>\n                  <Input\n                    id={`${formId}-email`}\n                    type='email'\n                    value={value.email}\n                    onChange={setEmail}\n                    placeholder='name@example.in'\n                  />\n                </div>\n              </div>\n\n              <div>\n                <Label className='mb-2 block'>Rating (required)</Label>\n                <FeedbackRating value={value.rating} onChange={setRating} />\n                {showErrors && missingRating && (\n                  <InputMessage state='error'>Choose a rating.</InputMessage>\n                )}\n              </div>\n\n              <div>\n                <Label htmlFor={`${formId}-message`} className='mb-2'>\n                  Feedback (required)\n                </Label>\n                <Textarea\n                  id={`${formId}-message`}\n                  value={value.message}\n                  onChange={setMessage}\n                  maxLength={maxLength}\n                  showCount\n                  state={showErrors && missingMessage ? 'error' : 'default'}\n                  placeholder='Tell us what happened'\n                />\n                {showErrors && missingMessage && (\n                  <InputMessage state='error'>Tell us what you would change.</InputMessage>\n                )}\n              </div>\n            </form>\n          </ModalBody>\n          <ModalFooter className='flex-col items-stretch gap-3 sm:flex-row sm:items-center'>\n            <Button type='button' variant='outlined' onClick={close} className='sm:flex-1'>\n              Cancel\n            </Button>\n            <Button type='submit' form={formId} className='sm:flex-1'>\n              Submit\n            </Button>\n          </ModalFooter>\n          {footer && <Body3 className='pb-4 text-center text-neutral-500'>{footer}</Body3>}\n        </ModalContent>\n      </Modal>\n    </div>\n  );\n}\n\nexport type { IFeedbackValue };\n\nexport { FeedbackWidget, FeedbackRating };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
