{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "subtask-03",
  "title": "Reorder by drag",
  "description": "Rows that move by their handle, by its arrow keys or by the up and down buttons, with the new position announced",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/avatar.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/subtask/subtask-03.tsx",
      "content": "'use client';\n\nimport { Reorder, useDragControls, type Transition } from 'motion/react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  ChevronDownIcon,\n  ChevronUpIcon,\n  GripVerticalIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport { personOf } from './_parts/people';\nimport { keyOf, SUBTASKS, type Subtask } from './_parts/subtask-data';\nimport { PersonAvatar, StatusGlyph } from './_parts/subtask-parts';\n\nconst FIRST_SUBTASKS = SUBTASKS.slice(0, 5);\n\nconst ROW_TRANSITION: Transition = { type: 'spring', bounce: 0, duration: 0.3 };\n\nconst NO_TRANSITION: Transition = { duration: 0 };\n\nconst STEP_BUTTON =\n  'text-muted-foreground hover:bg-foreground/5 hover:text-foreground focus-visible:ring-ring/25 inline-flex size-6 cursor-pointer items-center justify-center rounded-md outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-30';\n\nfunction rowTransitionOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return NO_TRANSITION;\n  }\n  return ROW_TRANSITION;\n}\n\nfunction moved(subtasks: Subtask[], index: number, step: number) {\n  const target = index + step;\n  const isOutside = target < 0 || target >= subtasks.length;\n  if (isOutside) {\n    return subtasks;\n  }\n  const next = [...subtasks];\n  const [item] = next.splice(index, 1);\n  next.splice(target, 0, item);\n  return next;\n}\n\nfunction stepOfKey(key: string) {\n  if (key === 'ArrowUp') {\n    return -1;\n  }\n  if (key === 'ArrowDown') {\n    return 1;\n  }\n  return 0;\n}\n\ntype FocusTarget = {\n  id: string;\n  control: 'grip' | 'up' | 'down';\n};\n\ntype RowProps = {\n  subtask: Subtask;\n  index: number;\n  total: number;\n  onMove: (step: number, control: FocusTarget['control']) => void;\n  registerControl: (\n    control: FocusTarget['control'],\n    node: HTMLButtonElement | null\n  ) => void;\n};\n\nfunction ReorderRow({\n  subtask,\n  index,\n  total,\n  onMove,\n  registerControl,\n}: RowProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const controls = useDragControls();\n  const [isDragging, setIsDragging] = useState(false);\n  const isFirst = index === 0;\n  const isLast = index === total - 1;\n  const position = index + 1;\n\n  function handleGripKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {\n    const step = stepOfKey(event.key);\n    if (step === 0) {\n      return;\n    }\n    event.preventDefault();\n    onMove(step, 'grip');\n  }\n\n  return (\n    <Reorder.Item\n      value={subtask}\n      dragListener={false}\n      dragControls={controls}\n      transition={rowTransitionOf(shouldReduceMotion)}\n      onDragStart={() => setIsDragging(true)}\n      onDragEnd={() => setIsDragging(false)}\n      className={cn(\n        'group/row bg-card relative flex h-10 items-center gap-2 rounded-lg ps-1 pe-1.5',\n        isDragging && 'surface-raised z-10'\n      )}\n    >\n      <button\n        type='button'\n        ref={(node) => registerControl('grip', node)}\n        onPointerDown={(event) => controls.start(event)}\n        onKeyDown={handleGripKeyDown}\n        aria-label={`Reorder ${subtask.title}, position ${position} of ${total}. Use the up and down arrow keys`}\n        className='text-muted-foreground hover:bg-foreground/5 hover:text-foreground focus-visible:ring-ring/25 inline-flex h-7 w-5 shrink-0 cursor-grab touch-none items-center justify-center rounded-md outline-none focus-visible:ring-[3px] active:cursor-grabbing'\n      >\n        <GripVerticalIcon className='size-4' />\n      </button>\n      <StatusGlyph status={subtask.status} />\n      <span className='text-muted-foreground w-16 shrink-0 text-xs tabular-nums'>\n        {keyOf(subtask.number)}\n      </span>\n      <span className='min-w-0 flex-1 truncate text-[13px]'>\n        {subtask.title}\n      </span>\n      <span className='flex shrink-0 items-center opacity-0 transition-opacity group-focus-within/row:opacity-100 group-hover/row:opacity-100 pointer-coarse:opacity-100'>\n        <button\n          type='button'\n          ref={(node) => registerControl('up', node)}\n          disabled={isFirst}\n          onClick={() => onMove(-1, 'up')}\n          aria-label={`Move ${subtask.title} up`}\n          className={STEP_BUTTON}\n        >\n          <ChevronUpIcon className='size-3.5' />\n        </button>\n        <button\n          type='button'\n          ref={(node) => registerControl('down', node)}\n          disabled={isLast}\n          onClick={() => onMove(1, 'down')}\n          aria-label={`Move ${subtask.title} down`}\n          className={STEP_BUTTON}\n        >\n          <ChevronDownIcon className='size-3.5' />\n        </button>\n      </span>\n      {subtask.assigneeId && (\n        <PersonAvatar person={personOf(subtask.assigneeId)} />\n      )}\n    </Reorder.Item>\n  );\n}\n\nfunction controlKeyOf(id: string, control: FocusTarget['control']) {\n  return `${id}:${control}`;\n}\n\nexport default function Subtask03() {\n  const [subtasks, setSubtasks] = useState(FIRST_SUBTASKS);\n  const [announcement, setAnnouncement] = useState('');\n  const [focusTarget, setFocusTarget] = useState<FocusTarget | null>(null);\n  const controlNodes = useRef(new Map<string, HTMLButtonElement>());\n\n  useEffect(() => {\n    if (!focusTarget) {\n      return;\n    }\n    const key = controlKeyOf(focusTarget.id, focusTarget.control);\n    const node = controlNodes.current.get(key);\n    const canFocus = node !== undefined && !node.disabled;\n    if (canFocus) {\n      node.focus();\n      return;\n    }\n    const grip = controlNodes.current.get(controlKeyOf(focusTarget.id, 'grip'));\n    grip?.focus();\n  }, [focusTarget, subtasks]);\n\n  function register(\n    id: string,\n    control: FocusTarget['control'],\n    node: HTMLButtonElement | null\n  ) {\n    const key = controlKeyOf(id, control);\n    if (node) {\n      controlNodes.current.set(key, node);\n      return;\n    }\n    controlNodes.current.delete(key);\n  }\n\n  function move(\n    subtask: Subtask,\n    step: number,\n    control: FocusTarget['control']\n  ) {\n    const index = subtasks.findIndex((item) => item.id === subtask.id);\n    const next = moved(subtasks, index, step);\n    if (next === subtasks) {\n      return;\n    }\n    const position = next.findIndex((item) => item.id === subtask.id) + 1;\n    setSubtasks(next);\n    setFocusTarget({ id: subtask.id, control });\n    setAnnouncement(\n      `${subtask.title} moved to position ${position} of ${next.length}`\n    );\n  }\n\n  return (\n    <section aria-label='Sub-tasks in order' className='w-full max-w-md'>\n      <header className='flex items-baseline justify-between gap-3 px-2'>\n        <h3 className='text-sm font-medium'>Sub-tasks</h3>\n        <p className='text-muted-foreground text-xs'>\n          Drag the handle or use its arrow keys\n        </p>\n      </header>\n      <Reorder.Group\n        axis='y'\n        values={subtasks}\n        onReorder={setSubtasks}\n        className='mt-2 flex flex-col gap-0.5'\n      >\n        {subtasks.map((subtask, index) => (\n          <ReorderRow\n            key={subtask.id}\n            subtask={subtask}\n            index={index}\n            total={subtasks.length}\n            onMove={(step, control) => move(subtask, step, control)}\n            registerControl={(control, node) =>\n              register(subtask.id, control, node)\n            }\n          />\n        ))}\n      </Reorder.Group>\n      <p aria-live='polite' className='sr-only'>\n        {announcement}\n      </p>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/subtask/subtask-03.tsx"
    },
    {
      "path": "registry/ndeko/components/subtask/_parts/motion-parts.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type MotionProps,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nexport const EASE_OUT_STRONG: [number, number, number, number] = [\n  0.23, 1, 0.32, 1,\n];\n\nexport const ENTER: Transition = { duration: 0.2, ease: EASE_OUT_STRONG };\n\nexport const EXIT: Transition = { duration: 0.15, ease: EASE_OUT_STRONG };\n\nexport type PresenceProps = Pick<\n  MotionProps,\n  'initial' | 'animate' | 'exit' | 'transition'\n>;\n\nconst HEIGHT_PRESENCE: PresenceProps = {\n  initial: { height: 0, opacity: 0 },\n  animate: { height: 'auto', opacity: 1 },\n  exit: { height: 0, opacity: 0, transition: EXIT },\n  transition: ENTER,\n};\n\nconst FADE_PRESENCE: PresenceProps = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0, transition: EXIT },\n  transition: ENTER,\n};\n\n/** Rows that arrive or leave a list: their height opens and closes so the rows after them slide. */\nexport function heightPresenceOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_PRESENCE;\n  }\n  return HEIGHT_PRESENCE;\n}\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: EASE_OUT_STRONG };\n\nconst SWAP_VARIANTS: Variants = {\n  hidden: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  shown: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  hidden: { opacity: 0 },\n  shown: { opacity: 1 },\n};\n\nfunction swapVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SWAP_VARIANTS;\n}\n\ntype SwapProps = {\n  swapKey: string;\n  children: React.ReactNode;\n  className?: string;\n};\n\n/** An icon or a short label that becomes another one in place. */\nexport function Swap({ swapKey, children, className }: SwapProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n\n  return (\n    <span className={cn('relative inline-flex', className)}>\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span\n          key={swapKey}\n          variants={swapVariantsOf(shouldReduceMotion)}\n          initial='hidden'\n          animate='shown'\n          exit='hidden'\n          transition={SWAP_TRANSITION}\n          className='inline-flex'\n        >\n          {children}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nconst COUNT_SLIDE = 8;\n\nconst COUNT_TRANSITION: Transition = { duration: 0.18, ease: EASE_OUT_STRONG };\n\nconst COUNT_VARIANTS: Variants = {\n  enter: (offset: number) => ({ y: offset, opacity: 0 }),\n  center: { y: 0, opacity: 1 },\n  leave: (offset: number) => ({ y: -offset, opacity: 0 }),\n};\n\nfunction directionOf(previous: number, next: number) {\n  if (next < previous) {\n    return -1;\n  }\n  return 1;\n}\n\nfunction offsetOf(direction: number, shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return 0;\n  }\n  return direction * COUNT_SLIDE;\n}\n\ntype RollingCountProps = {\n  value: number;\n  /** Writes the value, such as a duration; the digits still roll on the number. */\n  format?: (value: number) => string;\n  className?: string;\n};\n\nfunction plainNumber(value: number) {\n  return String(value);\n}\n\n/** A figure the person just changed: the new digits slide in from the side of the change. */\nexport function RollingCount({\n  value,\n  format = plainNumber,\n  className,\n}: RollingCountProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const [previous, setPrevious] = useState(value);\n  const [direction, setDirection] = useState(1);\n\n  if (previous !== value) {\n    setDirection(directionOf(previous, value));\n    setPrevious(value);\n  }\n\n  const offset = offsetOf(direction, shouldReduceMotion);\n\n  return (\n    <span\n      className={cn(\n        'relative inline-flex overflow-hidden tabular-nums',\n        className\n      )}\n    >\n      <AnimatePresence mode='popLayout' initial={false} custom={offset}>\n        <motion.span\n          key={value}\n          custom={offset}\n          variants={COUNT_VARIANTS}\n          initial='enter'\n          animate='center'\n          exit='leave'\n          transition={COUNT_TRANSITION}\n          className='inline-block'\n        >\n          {format(value)}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/subtask/_parts/motion-parts.tsx"
    },
    {
      "path": "registry/ndeko/components/subtask/_parts/people.ts",
      "content": "export type Person = {\n  id: string;\n  name: string;\n  firstName: string;\n};\n\nexport const CURRENT_USER_ID = 'amara';\n\nexport const PEOPLE: Person[] = [\n  {\n    id: 'amara',\n    name: 'Amara Okafor',\n    firstName: 'Amara',\n  },\n  {\n    id: 'leo',\n    name: 'Léo Martin',\n    firstName: 'Léo',\n  },\n  {\n    id: 'sana',\n    name: 'Sana Iqbal',\n    firstName: 'Sana',\n  },\n  {\n    id: 'tomas',\n    name: 'Tomás Rivera',\n    firstName: 'Tomás',\n  },\n  {\n    id: 'mei',\n    name: 'Mei Lin',\n    firstName: 'Mei',\n  },\n  {\n    id: 'jonah',\n    name: 'Jonah Brandt',\n    firstName: 'Jonah',\n  },\n];\n\nexport function personOf(id: string) {\n  const found = PEOPLE.find((person) => person.id === id);\n  if (found) {\n    return found;\n  }\n  return PEOPLE[0];\n}\n\nexport function isCurrentUser(id: string) {\n  return id === CURRENT_USER_ID;\n}\n\nexport function actorNameOf(id: string) {\n  if (isCurrentUser(id)) {\n    return 'You';\n  }\n  return personOf(id).name;\n}\n",
      "type": "registry:component",
      "target": "components/subtask/_parts/people.ts"
    },
    {
      "path": "registry/ndeko/components/subtask/_parts/subtask-data.ts",
      "content": "import type { StatusId } from './subtask-parts';\n\nexport type Subtask = {\n  id: string;\n  number: number;\n  title: string;\n  status: StatusId;\n  assigneeId?: string;\n};\n\nexport const TEAM_KEY = 'MOS';\n\nexport const SUBTASKS: Subtask[] = [\n  {\n    id: 'mos-215',\n    number: 215,\n    title: 'Pick the columns finance needs',\n    status: 'done',\n    assigneeId: 'leo',\n  },\n  {\n    id: 'mos-216',\n    number: 216,\n    title: 'Write the CSV writer with escaped fields',\n    status: 'done',\n    assigneeId: 'leo',\n  },\n  {\n    id: 'mos-218',\n    number: 218,\n    title: 'Add the totals row',\n    status: 'in_progress',\n    assigneeId: 'leo',\n  },\n  {\n    id: 'mos-219',\n    number: 219,\n    title: 'Respect the account time zone in dates',\n    status: 'in_review',\n    assigneeId: 'tomas',\n  },\n  {\n    id: 'mos-220',\n    number: 220,\n    title: 'Download button on the invoices page',\n    status: 'todo',\n    assigneeId: 'mei',\n  },\n  {\n    id: 'mos-222',\n    number: 222,\n    title: 'Test with ten thousand invoices',\n    status: 'done',\n    assigneeId: 'sana',\n  },\n  {\n    id: 'mos-223',\n    number: 223,\n    title: 'Note the export in the changelog',\n    status: 'backlog',\n  },\n];\n\nexport function keyOf(number: number) {\n  return `${TEAM_KEY}-${number}`;\n}\n\nexport function isDone(subtask: Subtask) {\n  return subtask.status === 'done';\n}\n\nexport function doneCountOf(subtasks: Subtask[]) {\n  return subtasks.filter(isDone).length;\n}\n\nexport function withToggled(subtasks: Subtask[], subtaskId: string) {\n  return subtasks.map((subtask) => {\n    if (subtask.id !== subtaskId) {\n      return subtask;\n    }\n    return { ...subtask, status: toggledStatusOf(subtask.status) };\n  });\n}\n\nfunction toggledStatusOf(status: StatusId): StatusId {\n  if (status === 'done') {\n    return 'todo';\n  }\n  return 'done';\n}\n\nexport function countLabel(count: number, singular: string, plural: string) {\n  if (count === 1) {\n    return `${count} ${singular}`;\n  }\n  return `${count} ${plural}`;\n}\n",
      "type": "registry:component",
      "target": "components/subtask/_parts/subtask-data.ts"
    },
    {
      "path": "registry/ndeko/components/subtask/_parts/subtask-parts.tsx",
      "content": "'use client';\n\nimport { CheckIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Avatar } from '@/registry/ndeko/ui/avatar';\n\nimport { Swap } from './motion-parts';\nimport type { Person } from './people';\n\nexport type StatusId =\n  'backlog' | 'todo' | 'in_progress' | 'in_review' | 'done';\n\nexport const STATUSES: { id: StatusId; name: string; color: string }[] = [\n  { id: 'backlog', name: 'Backlog', color: 'var(--muted-foreground)' },\n  { id: 'todo', name: 'Todo', color: 'var(--muted-foreground)' },\n  { id: 'in_progress', name: 'In progress', color: 'var(--warning)' },\n  { id: 'in_review', name: 'In review', color: 'var(--primary)' },\n  { id: 'done', name: 'Done', color: 'var(--success)' },\n];\n\nexport function statusOf(id: StatusId) {\n  const found = STATUSES.find((status) => status.id === id);\n  if (found) {\n    return found;\n  }\n  return STATUSES[0];\n}\n\nexport function isStatusId(value: unknown): value is StatusId {\n  return STATUSES.some((status) => status.id === value);\n}\n\nconst PIE_RADIUS = 2;\n\nconst PIE_CIRCUMFERENCE = 2 * Math.PI * PIE_RADIUS;\n\nfunction StartedShape({ share }: { share: number }) {\n  const filled = share * PIE_CIRCUMFERENCE;\n  return (\n    <>\n      <circle cx='7' cy='7' r='6' stroke='currentColor' strokeWidth='1.5' />\n      <circle\n        cx='7'\n        cy='7'\n        r={PIE_RADIUS}\n        stroke='currentColor'\n        strokeWidth={PIE_RADIUS * 2}\n        strokeDasharray={`${filled} ${PIE_CIRCUMFERENCE}`}\n        transform='rotate(-90 7 7)'\n      />\n    </>\n  );\n}\n\nfunction statusShapeOf(status: StatusId) {\n  if (status === 'backlog') {\n    return (\n      <circle\n        cx='7'\n        cy='7'\n        r='6'\n        stroke='currentColor'\n        strokeWidth='1.5'\n        strokeDasharray='1.6 1.9'\n      />\n    );\n  }\n  if (status === 'todo') {\n    return (\n      <circle cx='7' cy='7' r='6' stroke='currentColor' strokeWidth='1.5' />\n    );\n  }\n  if (status === 'in_progress') {\n    return <StartedShape share={0.5} />;\n  }\n  if (status === 'in_review') {\n    return <StartedShape share={0.75} />;\n  }\n  return (\n    <>\n      <circle cx='7' cy='7' r='7' fill='currentColor' />\n      <path\n        d='M4.4 7.2 6.2 9l3.4-3.8'\n        stroke='var(--card)'\n        strokeWidth='1.6'\n        strokeLinecap='round'\n        strokeLinejoin='round'\n      />\n    </>\n  );\n}\n\nexport function StatusGlyph({\n  status,\n  className,\n}: {\n  status: StatusId;\n  className?: string;\n}) {\n  const glyphStyle = { color: statusOf(status).color } as React.CSSProperties;\n  return (\n    <svg\n      viewBox='0 0 14 14'\n      fill='none'\n      aria-hidden='true'\n      style={glyphStyle}\n      className={cn('size-3.5 shrink-0', className)}\n    >\n      {statusShapeOf(status)}\n    </svg>\n  );\n}\n\nexport function PersonAvatar({\n  person,\n  className,\n}: {\n  person: Person;\n  className?: string;\n}) {\n  return (\n    <Avatar className={cn('size-5 text-[9px]', className)} seed={person.name} />\n  );\n}\n\nconst RING_RADIUS = 6.5;\n\nconst RING_LENGTH = 2 * Math.PI * RING_RADIUS;\n\nfunction shareOf(done: number, total: number) {\n  if (total === 0) {\n    return 0;\n  }\n  return done / total;\n}\n\nfunction ringKeyOf(isComplete: boolean) {\n  if (isComplete) {\n    return 'complete';\n  }\n  return 'ring';\n}\n\n/** The share of sub-tasks done; the arc travels on each check and becomes a check mark once all are done. */\nexport function ProgressRing({\n  done,\n  total,\n  className,\n}: {\n  done: number;\n  total: number;\n  className?: string;\n}) {\n  const share = shareOf(done, total);\n  const isComplete = total > 0 && done === total;\n  const dashOffset = RING_LENGTH * (1 - share);\n\n  return (\n    <Swap swapKey={ringKeyOf(isComplete)} className={className}>\n      {isComplete && (\n        <span className='bg-success/15 text-success-foreground flex size-4 items-center justify-center rounded-full'>\n          <CheckIcon strokeWidth={3} className='size-2.5' />\n        </span>\n      )}\n      {!isComplete && (\n        <svg\n          viewBox='0 0 16 16'\n          aria-hidden='true'\n          className='size-4 -rotate-90'\n        >\n          <circle\n            cx='8'\n            cy='8'\n            r={RING_RADIUS}\n            fill='none'\n            strokeWidth='2'\n            className='stroke-foreground/10'\n          />\n          <circle\n            cx='8'\n            cy='8'\n            r={RING_RADIUS}\n            fill='none'\n            strokeWidth='2'\n            strokeLinecap='round'\n            strokeDasharray={RING_LENGTH}\n            strokeDashoffset={dashOffset}\n            className='stroke-primary transition-[stroke-dashoffset] duration-300 ease-(--ease-out-strong)'\n          />\n        </svg>\n      )}\n    </Swap>\n  );\n}\n\n/** A title that gets struck through from left to right when its sub-task is done. */\nexport function StrikeTitle({\n  title,\n  isDone,\n  className,\n}: {\n  title: string;\n  isDone: boolean;\n  className?: string;\n}) {\n  return (\n    <span\n      className={cn(\n        'relative min-w-0 truncate text-[13px] transition-colors duration-200',\n        isDone && 'text-muted-foreground',\n        className\n      )}\n    >\n      {title}\n      <span\n        aria-hidden='true'\n        className={cn(\n          'bg-foreground/35 pointer-events-none absolute inset-x-0 top-1/2 h-px origin-left scale-x-0 transition-transform duration-200 ease-(--ease-out-strong)',\n          isDone && 'scale-x-100'\n        )}\n      />\n    </span>\n  );\n}\n\n/** One segment per sub-task, filled from the left when it is done. */\nexport function SegmentMeter({\n  states,\n  className,\n}: {\n  states: boolean[];\n  className?: string;\n}) {\n  return (\n    <span aria-hidden='true' className={cn('flex gap-0.5', className)}>\n      {states.map((isDone, index) => (\n        <span\n          key={index}\n          className='bg-foreground/10 relative h-1.5 min-w-1 flex-1 overflow-hidden rounded-full'\n        >\n          <span\n            className={cn(\n              'bg-success absolute inset-0 origin-left scale-x-0 rounded-full transition-transform duration-200 ease-(--ease-out-strong)',\n              isDone && 'scale-x-100'\n            )}\n          />\n        </span>\n      ))}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/subtask/_parts/subtask-parts.tsx"
    }
  ],
  "categories": [
    "components",
    "subtask"
  ],
  "type": "registry:component"
}