{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "subtask-08",
  "title": "Live list",
  "description": "A checklist a team writes in at once: new items arrive in their group with the name of who added them, ticked ones settle into Done, and the card keeps its height.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/avatar.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/checkbox.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/input.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/segments.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/use-timeouts.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/subtask/subtask-08.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  motion,\n  type Transition,\n} from 'motion/react';\nimport { useRef, useState } from 'react';\n\nimport {\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { useTimeouts } from '@/registry/ndeko/hooks/use-timeouts';\nimport { hold } from '@/registry/ndeko/lib/demonstration';\nimport { PlusIcon } from '@/registry/ndeko/lib/icons';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Checkbox } from '@/registry/ndeko/ui/checkbox';\nimport { Input } from '@/registry/ndeko/ui/input';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { Segments, type Segment } from '@/registry/ndeko/ui/segments';\n\nimport { RollingCount } from './_parts/motion-parts';\nimport { actorNameOf, CURRENT_USER_ID, personOf } from './_parts/people';\nimport { PersonAvatar, StrikeTitle } from './_parts/subtask-parts';\n\ntype OpenGroup = 'prep' | 'launch';\n\ntype GroupId = OpenGroup | 'done';\n\ntype LiveItem = {\n  id: string;\n  title: string;\n  group: OpenGroup;\n  addedBy: string;\n  isDone: boolean;\n  doneBy?: string;\n  doneOrder: number;\n};\n\ntype Typing = {\n  personId: string;\n  group: OpenGroup;\n};\n\nconst GROUPS: { id: GroupId; name: string; empty: string }[] = [\n  { id: 'prep', name: 'Prep', empty: 'Nothing to prepare yet' },\n  { id: 'launch', name: 'Launch', empty: 'Nothing planned for launch yet' },\n  { id: 'done', name: 'Done', empty: 'Ticked items land here' },\n];\n\nconst ADD_GROUPS: Segment<OpenGroup>[] = [\n  { value: 'prep', label: 'Prep' },\n  { value: 'launch', label: 'Launch' },\n];\n\nconst MAX_ITEMS = 9;\nconst FRESH_FOR_MS = 1800;\n\nconst RESTING_ITEMS: LiveItem[] = [\n  {\n    id: 'prep-notes',\n    title: 'Write the release notes',\n    group: 'prep',\n    addedBy: 'sana',\n    isDone: false,\n    doneOrder: 0,\n  },\n  {\n    id: 'prep-pricing',\n    title: 'Update the pricing page copy',\n    group: 'prep',\n    addedBy: 'leo',\n    isDone: false,\n    doneOrder: 0,\n  },\n  {\n    id: 'launch-email',\n    title: 'Schedule the announcement email',\n    group: 'launch',\n    addedBy: 'mei',\n    isDone: false,\n    doneOrder: 0,\n  },\n  {\n    id: 'prep-freeze',\n    title: 'Freeze the feature list',\n    group: 'prep',\n    addedBy: 'tomas',\n    isDone: true,\n    doneBy: 'tomas',\n    doneOrder: 1,\n  },\n];\n\nconst TRAVEL: Transition = { type: 'spring', bounce: 0, duration: 0.35 };\n\nconst INSTANT: Transition = { duration: 0 };\n\nfunction travelOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return INSTANT;\n  }\n  return TRAVEL;\n}\n\nfunction layoutIdOf(id: string, shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return undefined;\n  }\n  return `live-${id}`;\n}\n\nfunction arrivalOf(isFresh: boolean) {\n  if (isFresh) {\n    return { opacity: 0 };\n  }\n  return false;\n}\n\nfunction groupOf(item: LiveItem): GroupId {\n  if (item.isDone) {\n    return 'done';\n  }\n  return item.group;\n}\n\nfunction itemsInGroup(items: LiveItem[], group: GroupId) {\n  const inGroup = items.filter((item) => groupOf(item) === group);\n  if (group !== 'done') {\n    return inGroup;\n  }\n  return [...inGroup].sort(\n    (first, second) => second.doneOrder - first.doneOrder\n  );\n}\n\nfunction signatureOf(items: LiveItem[]) {\n  return items.map((item) => `${item.id}:${groupOf(item)}`).join('|');\n}\n\nfunction metaPersonIdOf(item: LiveItem) {\n  if (item.isDone && item.doneBy) {\n    return item.doneBy;\n  }\n  return item.addedBy;\n}\n\nfunction metaLabelOf(item: LiveItem) {\n  const name = actorNameOf(metaPersonIdOf(item));\n  if (item.isDone) {\n    return `Ticked by ${name}`;\n  }\n  return `Added by ${name}`;\n}\n\nfunction shortNameOf(personId: string) {\n  if (personId === CURRENT_USER_ID) {\n    return 'You';\n  }\n  return personOf(personId).firstName;\n}\n\nfunction withoutOldest(items: LiveItem[]) {\n  const oldestDone = items.find((item) => item.isDone);\n  const dropped = oldestDone ?? items[0];\n  return items.filter((item) => item.id !== dropped.id);\n}\n\nfunction withCap(items: LiveItem[]) {\n  if (items.length <= MAX_ITEMS) {\n    return items;\n  }\n  return withoutOldest(items);\n}\n\nfunction groupNameOf(group: OpenGroup) {\n  if (group === 'launch') {\n    return 'Launch';\n  }\n  return 'Prep';\n}\n\ntype LiveRowProps = {\n  item: LiveItem;\n  isFresh: boolean;\n  signature: string;\n  onToggle: () => void;\n};\n\nfunction LiveRow({ item, isFresh, signature, onToggle }: LiveRowProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const metaId = metaPersonIdOf(item);\n\n  return (\n    <motion.li\n      layout='position'\n      layoutId={layoutIdOf(item.id, shouldReduceMotion)}\n      layoutDependency={signature}\n      initial={arrivalOf(isFresh)}\n      animate={{ opacity: 1 }}\n      transition={travelOf(shouldReduceMotion)}\n      data-fresh={isFresh}\n      className='group/row'\n    >\n      <label className='hover:bg-foreground/[0.03] group-data-[fresh=true]/row:bg-primary/10 flex h-9 cursor-pointer items-center gap-3 rounded-lg px-3 transition-colors duration-1000'>\n        <Checkbox checked={item.isDone} onCheckedChange={onToggle} />\n        <StrikeTitle title={item.title} isDone={item.isDone} />\n        <span\n          aria-label={metaLabelOf(item)}\n          className='text-muted-foreground ms-auto flex shrink-0 items-center gap-1.5 text-xs'\n        >\n          <PersonAvatar person={personOf(metaId)} />\n          <span className='hidden max-w-16 truncate @xs:inline'>\n            {shortNameOf(metaId)}\n          </span>\n        </span>\n      </label>\n    </motion.li>\n  );\n}\n\ntype GroupProps = {\n  name: string;\n  empty: string;\n  items: LiveItem[];\n  freshId: string | null;\n  signature: string;\n  onToggle: (id: string) => void;\n};\n\nfunction Group({\n  name,\n  empty,\n  items,\n  freshId,\n  signature,\n  onToggle,\n}: GroupProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const hasItems = items.length > 0;\n\n  return (\n    <section aria-label={name} className='flex flex-col'>\n      <motion.div\n        layout='position'\n        layoutDependency={signature}\n        transition={travelOf(shouldReduceMotion)}\n        className='flex h-7 items-center justify-between px-3'\n      >\n        <h3 className='text-muted-foreground text-xs font-medium'>{name}</h3>\n        <span className='text-muted-foreground text-xs'>\n          <RollingCount value={items.length} />\n        </span>\n      </motion.div>\n      <ul>\n        {items.map((item) => (\n          <LiveRow\n            key={item.id}\n            item={item}\n            isFresh={item.id === freshId}\n            signature={signature}\n            onToggle={() => onToggle(item.id)}\n          />\n        ))}\n      </ul>\n      {!hasItems && (\n        <p className='text-muted-foreground px-3 py-2 text-[13px]'>{empty}</p>\n      )}\n    </section>\n  );\n}\n\nfunction enterScaleOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return 1;\n  }\n  return 0.96;\n}\n\nfunction TypingStrip({ typing }: { typing: Typing | null }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const enter = { opacity: 0, y: 4, scale: enterScaleOf(shouldReduceMotion) };\n\n  return (\n    <div className='flex h-6 items-center px-3'>\n      <AnimatePresence mode='popLayout' initial={false}>\n        {typing && (\n          <motion.span\n            key={typing.personId}\n            initial={enter}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, transition: { duration: 0.1 } }}\n            transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n            className='text-muted-foreground flex items-center gap-1.5 text-xs'\n          >\n            <PersonAvatar person={personOf(typing.personId)} />\n            {shortNameOf(typing.personId)} is adding to{' '}\n            {groupNameOf(typing.group)}\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default function Subtask08() {\n  const [items, setItems] = useState(RESTING_ITEMS);\n  const [typing, setTyping] = useState<Typing | null>(null);\n  const [freshId, setFreshId] = useState<string | null>(null);\n  const [draft, setDraft] = useState('');\n  const [addGroup, setAddGroup] = useState<OpenGroup>('prep');\n  const [announcement, setAnnouncement] = useState('');\n  const counter = useRef({ items: 0, ticks: 1 });\n  const { schedule, clearAll } = useTimeouts();\n  const signature = signatureOf(items);\n  const doneCount = items.filter((item) => item.isDone).length;\n\n  function addItem(title: string, group: OpenGroup, personId: string) {\n    counter.current.items += 1;\n    const id = `live-${counter.current.items}`;\n    const item: LiveItem = {\n      id,\n      title,\n      group,\n      addedBy: personId,\n      isDone: false,\n      doneOrder: 0,\n    };\n    setItems((current) => withCap([...current, item]));\n    setTyping(null);\n    setFreshId(id);\n    schedule(() => setFreshId(null), FRESH_FOR_MS);\n  }\n\n  function setTicked(id: string, personId: string) {\n    counter.current.ticks += 1;\n    const order = counter.current.ticks;\n    setItems((current) =>\n      current.map((item) => {\n        if (item.id !== id) {\n          return item;\n        }\n        if (item.isDone) {\n          return { ...item, isDone: false, doneBy: undefined, doneOrder: 0 };\n        }\n        return { ...item, isDone: true, doneBy: personId, doneOrder: order };\n      })\n    );\n  }\n\n  function toggleFromPerson(id: string) {\n    const item = items.find((candidate) => candidate.id === id);\n    setTicked(id, CURRENT_USER_ID);\n    if (!item) {\n      return;\n    }\n    if (item.isDone) {\n      setAnnouncement(`${item.title} is open again`);\n      return;\n    }\n    setAnnouncement(`${item.title} moved to Done`);\n  }\n\n  function submitDraft(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    const title = draft.trim();\n    if (title === '') {\n      return;\n    }\n    addItem(title, addGroup, CURRENT_USER_ID);\n    setDraft('');\n    setAnnouncement(`${title} added to ${groupNameOf(addGroup)}`);\n  }\n\n  function startOver() {\n    clearAll();\n    setItems(RESTING_ITEMS);\n    setTyping(null);\n    setFreshId(null);\n  }\n\n  const steps: AutoplayStep[] = [\n    { after: 2200, run: () => setTyping({ personId: 'leo', group: 'launch' }) },\n    {\n      after: 1100,\n      run: () => addItem('Confirm the status page text', 'launch', 'leo'),\n    },\n    { after: 1800, run: () => setTicked('prep-notes', 'sana') },\n    { after: 1600, run: () => setTyping({ personId: 'mei', group: 'prep' }) },\n    {\n      after: 1000,\n      run: () => addItem('Book the launch call', 'prep', 'mei'),\n    },\n    { after: 1800, run: () => setTicked('launch-email', 'tomas') },\n    { after: 1800, run: () => setTicked('prep-pricing', 'sana') },\n    { after: 1600, run: hold },\n  ];\n  const { ref } = useAutoplay(steps, {\n    loopAfter: 2600,\n    onReset: startOver,\n  });\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='bg-foreground/[0.025] @container flex h-[27rem] w-full max-w-md flex-col rounded-xl p-1'\n    >\n      <div className='flex items-baseline justify-between gap-2 px-3 pt-2 pb-1'>\n        <h3 className='text-sm font-medium'>Launch checklist</h3>\n        <span className='text-muted-foreground text-xs'>\n          <RollingCount value={doneCount} /> of {items.length} done\n        </span>\n      </div>\n      <LayoutGroup>\n        <div className='min-h-0 flex-1 overflow-y-auto'>\n          {GROUPS.map((group) => (\n            <Group\n              key={group.id}\n              name={group.name}\n              empty={group.empty}\n              items={itemsInGroup(items, group.id)}\n              freshId={freshId}\n              signature={signature}\n              onToggle={toggleFromPerson}\n            />\n          ))}\n        </div>\n      </LayoutGroup>\n      <TypingStrip typing={typing} />\n      <form\n        onSubmit={submitDraft}\n        className='flex flex-wrap items-center gap-2 px-2 pt-1 pb-2'\n      >\n        <Input\n          aria-label='New item'\n          placeholder={`Add to ${groupNameOf(addGroup)}`}\n          value={draft}\n          onChange={(event) => setDraft(event.currentTarget.value)}\n          className='min-w-40 flex-1'\n        />\n        <Segments\n          label='Group'\n          value={addGroup}\n          segments={ADD_GROUPS}\n          onValueChange={setAddGroup}\n        />\n        <Button\n          type='submit'\n          size='icon'\n          aria-label='Add item'\n          disabled={draft.trim() === ''}\n          className='shrink-0'\n        >\n          <PlusIcon />\n        </Button>\n      </form>\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/subtask/subtask-08.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-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"
}