{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawn-check",
  "title": "Drawn Check",
  "description": "A check on a small chip that draws its stroke when it lands, after a short pop, and leaves faster than it came; reduced motion shows it drawn.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/motion-presets.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/drawn-check.tsx",
      "content": "'use client';\n\nimport { motion, type Transition, type Variants } from 'motion/react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { EASE_OUT_STRONG } from '@/registry/ndeko/lib/motion-presets';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nexport type DrawnCheckTone = 'primary' | 'success';\n\nexport type DrawnCheckEntrance = 'pop' | 'draw' | 'draw-in-view';\n\nexport type DrawnCheckPace = 'quick' | 'calm';\n\nconst CHIP =\n  'chip-3d inline-flex size-[18px] shrink-0 items-center justify-center rounded-full';\n\nconst TONE_CLASS: Record<DrawnCheckTone, string> = {\n  primary: '[--chip:var(--primary)]',\n  success: '[--chip:var(--success)]',\n};\n\nconst STROKE = {\n  d: 'M2.6 6.4 4.9 8.7 9.5 3.6',\n  fill: 'none',\n  stroke: 'currentColor',\n  strokeWidth: '1.9',\n  strokeLinecap: 'round',\n  strokeLinejoin: 'round',\n} as const;\n\nconst DRAW: Record<DrawnCheckPace, Transition> = {\n  quick: { duration: 0.28, ease: EASE_OUT_STRONG, delay: 0.08 },\n  calm: { duration: 0.32, ease: EASE_OUT_STRONG, delay: 0.1 },\n};\n\nconst POP: Transition = { duration: 0.16, ease: EASE_OUT_STRONG };\n\nconst LEAVE: Transition = { duration: 0.1 };\n\nconst DRAWN = { pathLength: 1 };\n\nconst NOT_DRAWN = { pathLength: 0 };\n\nconst SHOWN = { opacity: 1, scale: 1 };\n\nconst LEFT = { opacity: 0, transition: LEAVE };\n\nconst ONCE = { once: true };\n\nconst SEEN_STROKE: Record<DrawnCheckPace, Variants> = {\n  quick: { waiting: NOT_DRAWN, drawn: { ...DRAWN, transition: DRAW.quick } },\n  calm: { waiting: NOT_DRAWN, drawn: { ...DRAWN, transition: DRAW.calm } },\n};\n\nconst SEEN_STROKE_STILL: Variants = { waiting: DRAWN, drawn: DRAWN };\n\nfunction strokeStartOf(shouldReduceMotion: boolean) {\n  if (shouldReduceMotion) {\n    return DRAWN;\n  }\n  return NOT_DRAWN;\n}\n\nfunction seenStrokeOf(shouldReduceMotion: boolean, pace: DrawnCheckPace) {\n  if (shouldReduceMotion) {\n    return SEEN_STROKE_STILL;\n  }\n  return SEEN_STROKE[pace];\n}\n\nfunction arrivalOf(shouldReduceMotion: boolean) {\n  if (shouldReduceMotion) {\n    return { opacity: 0, scale: 1 };\n  }\n  return { opacity: 0, scale: 0.7 };\n}\n\ntype StrokeProps = {\n  pace: DrawnCheckPace;\n  shouldReduceMotion: boolean;\n};\n\nfunction MountedStroke({ pace, shouldReduceMotion }: StrokeProps) {\n  return (\n    <svg aria-hidden='true' viewBox='0 0 12 12' className='size-3'>\n      <motion.path\n        {...STROKE}\n        initial={strokeStartOf(shouldReduceMotion)}\n        animate={DRAWN}\n        transition={DRAW[pace]}\n      />\n    </svg>\n  );\n}\n\n/** Takes its cue from the chip, which carries `whileInView`. */\nfunction SeenStroke({ pace, shouldReduceMotion }: StrokeProps) {\n  return (\n    <svg aria-hidden='true' viewBox='0 0 12 12' className='size-3'>\n      <motion.path\n        {...STROKE}\n        variants={seenStrokeOf(shouldReduceMotion, pace)}\n      />\n    </svg>\n  );\n}\n\nexport type DrawnCheckProps = {\n  tone?: DrawnCheckTone;\n  /** Read by screen readers. Leave it out when the row already says it is done or selected. */\n  label?: string;\n  /**\n   * `pop`: the chip grows in, its check draws, and it leaves faster than it came (inside\n   * `AnimatePresence`). `draw`: the chip is there at once and only its check draws.\n   * `draw-in-view`: the same, the first time the mark is on screen rather than when it mounts, for\n   * a list that can be hidden at the moment the mark lands.\n   */\n  entrance?: DrawnCheckEntrance;\n  /** `quick` draws the stroke in 280ms after 80ms, `calm` in 320ms after 100ms. */\n  pace?: DrawnCheckPace;\n  className?: string;\n};\n\n/**\n * A check that draws itself on a small chip. Reduced motion shows it drawn. Inside an\n * `AnimatePresence initial={false}`, a mark present on mount is simply there.\n */\nexport function DrawnCheck({\n  tone = 'primary',\n  label,\n  entrance = 'pop',\n  pace = 'quick',\n  className,\n}: DrawnCheckProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const hasLabel = Boolean(label);\n  const chipClass = cn(CHIP, TONE_CLASS[tone], className);\n\n  if (entrance === 'draw-in-view') {\n    return (\n      <motion.span\n        aria-hidden={!hasLabel}\n        initial='waiting'\n        whileInView='drawn'\n        viewport={ONCE}\n        className={chipClass}\n      >\n        <SeenStroke pace={pace} shouldReduceMotion={shouldReduceMotion} />\n        {hasLabel && <span className='sr-only'>{label}</span>}\n      </motion.span>\n    );\n  }\n  if (entrance === 'draw') {\n    return (\n      <span aria-hidden={!hasLabel} className={chipClass}>\n        <MountedStroke pace={pace} shouldReduceMotion={shouldReduceMotion} />\n        {hasLabel && <span className='sr-only'>{label}</span>}\n      </span>\n    );\n  }\n  return (\n    <motion.span\n      aria-hidden={!hasLabel}\n      initial={arrivalOf(shouldReduceMotion)}\n      animate={SHOWN}\n      exit={LEFT}\n      transition={POP}\n      className={chipClass}\n    >\n      <MountedStroke pace={pace} shouldReduceMotion={shouldReduceMotion} />\n      {hasLabel && <span className='sr-only'>{label}</span>}\n    </motion.span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "ai"
  ],
  "type": "registry:ui"
}